Summary: The Scripting Guys discuss three different approaches to finding disabled user accounts in Active Directory Domain Services by using Windows PowerShell.
Hey, Scripting Guy! I would like to use Windows PowerShell to search Active Directory Domain Services (AD DS) for user accounts that are disabled. I had a VBScript script I had obtained from the Scripting Guys a long time ago, but it is confusing and I would like to find a different approach if possible. We are migrating our scripting environment from VBScript to Windows PowerShell, so I would like to replace this script if possible.
— EJ
Hello EJ,
Microsoft Scripting Guy Ed Wilson here I am having flashbacks of currywurst, cool breezes coming across the Alster, long walks through green meadows, and long talks into the evening with awesome friends. I was writing the chapters on Active Directory of my Windows PowerShell Scripting Guide book for Microsoft Press when I arrived in Hamburg on the ICE Train from Frankfurt. I took the following photo on a walk around the lake. The breeze caused the fountain to spray mist creating a rainbow near the water.
The ICE trains all have power outlets in them, which meant I was able to work on my laptop the entire train ride. Using Virtual PC 2007, I fired up an Active Directory domain controller and a workstation in the same domain, and set about writing Active Directory Service Interfaces (ADSI) code to locate disabled user accounts.
The script I wrote to find disabled user accounts in my domain uses the Win32_UserAccount WMI class. It is a very easy class to use in Windows PowerShell, and it works well for small domains. The script listed here also appeared in the Windows Server 2008 Security Resource Kit that was part of the Windows Server 2008 Resource Kit (I wrote all 150 Windows PowerShell scripts that appear in those volumes).
For other articles about searching Active Directory using Windows PowerShell, see this collection of Hey, Scripting Guy! Blog posts.
LocateDisabledUsers.ps1
param(
$domain=$env:userdomain,
[switch]$query,
[switch]$whatif,
[switch]$help,
[switch]$examples,
[switch]$min,
[switch]$full
) #end param
# Begin Functions
function funHelp()
{
$descriptionText= `
@”
NAME: LocateDisabledUsers.ps1
DESCRIPTION:
Locates disabled users a local or remote domain by
supplying the netbios name of the domain.
The script can query multiple domains by accepting
more than one value for the -domain parameter. The
script also supports using -whatif to prototype the
command prior to execution
PARAMETERS:
-domain the domain or domains to query for locked
out users. Note: this is the netbios domain name.
Does not accept fully qualified domain name. For
example: nwtraders is correct, nwtraders.com is
not.
-query executes the query
-whatif prototypes the command.
-help prints help description and parameters file
-examples prints only help examples of syntax
-full prints complete help information
-min prints minimal help. Modifies -help
“@ #end descriptionText
$examplesText= `
@”
SYNTAX:
LocateDisabledUsers.ps1
Displays an error missing parameter, and calls help
LocateDisabledUsers.ps1 -query
Queries disabled user accounts. The domain queried is
the local logged on users domain from the machine
that launched the script
LocateDisabledUsers.ps1 -domain nwtraders, contoso -query
Queries disabled user accounts in the nwtraders domain and
in the contoso domain. The script is executed locally
LocateDisabledUsers.ps1 -query -domain nwtraders -whatif
Displays what if: Perform operation locate disabled
users from the nwtraders domain.The query will execute
from the localhost computer
LocateDisabledUsers.ps1 -help
Prints the help topic for the script
LocateDisabledUsers.ps1 -help -full
Prints full help topic for the script
LocateDisabledUsers.ps1 -help -examples
Prints only the examples for the script
LocateDisabledUsers.ps1 -examples
Prints only the examples for the script
“@ #end examplesText
$remarks = `
”
REMARKS
For more information, type: $($MyInvocation.ScriptName) -help -full
” #end remarks
if($examples) { $examplesText ; $remarks ; exit }
if($full) { $descriptionText; $examplesText ; exit }
if($min) { $descriptionText ; exit }
$descriptionText; $remarks
exit
} #end funHelp function
function funline (
$strIN,
$char = “=”,
$sColor = “Yellow”,
$uColor = “darkYellow”,
[switch]$help
)
{
if($help)
{
$local:helpText = `
@”
Funline accepts inputs: -strIN for input string and -char for seperator
-sColor for the string color, and -uColor for the underline color. Only
the -strIn is required. The others have the following default values:
-char: =, -sColor: Yellow, -uColor: darkYellow
Example:
funline -strIN “Hello world”
funline -strIn “Morgen welt” -char “–” -sColor “blue” -uColor “yellow”
funline -help
“@
$local:helpText
break
} #end funline help
$strLine= $char * $strIn.length
Write-Host -ForegroundColor $sColor $strIN
Write-Host -ForegroundColor $uColor $strLine
} #end funLine function
Function funWhatIf()
{
foreach($sDomain in $Domain)
{
“what if: Perform operation locate disabled users from the $sDomain domain”
}
exit
} #end funWhatIf
Function funQuery()
{
Foreach($sDomain in $domain)
{
$strOutput = Get-WmiObject -Class win32_useraccount -filter `
“domain = “”$sDomain”” AND disabled = ‘true'”
$count = ($strOutput | Measure-Object).count
If($count -eq 0)
{
funline -scolor green -ucolor darkyellow -strIN `
“There are no disabled accounts in the $sDomain”
} #end if
ELSE
{
funline -scolor red -ucolor darkyellow -strIN `
“$count disabled in the $sDomain domain — List follows:”
format-table -property name, sid -AutoSize -inputobject $strOutput
} #end else
} #end foreach
exit
} #end funquery
# Entry Point
if($help) { funhelp }
if($examples) { funhelp }
if($full) { funhelp }
if($whatif) { funWhatIf }
if(!$query) { “missing parameter” ; funhelp }
if($query) { funQuery }
When the LocateDisabledUsers.ps1 script runs, the output appears that is shown in the following image.
Later in my Windows PowerShell 2.0 Best Practices book, I decided to update the script, and I used the [adsiSearcher] type accelerator. The output is not exactly the same because I do not return the user’s SID. In addition, I display both the disabled user accounts and the accounts that are not disabled, but the concept is basically the same. The FindDisabledUserAccounts.ps1 script is shown here.
FindDisabledUserAccounts.ps1
#Requires -Version 2.0
$filter = “(&(objectClass=user)(objectCategory=person))”
$users = ([adsiSearcher]$Filter).findall()
foreach($suser in $users)
{
“Testing $($suser.properties.item(“”distinguishedname””))”
$user = [adsi]“LDAP://$($suser.properties.item(“”distinguishedname””))”
$uac=$user.psbase.invokeget(“useraccountcontrol”)
if($uac -band 0x2)
{ write-host -foregroundcolor red “`t account is disabled” }
ELSE
{ write-host -foregroundcolor green “`t account is not disabled” }
} #foreach
When the FindDisabledUserAccounts.ps1 script runs, the output appears that is shown in the following image.
Now with access to the Active Directory module from Windows Server 2008 R2 and with my desktop running Windows 7 with the RSAT tools installed, I can use the Search-ADAccount cmdlet. I have not turned this into a script yet because it is basically a single command from a cmdlet.
For a good introduction to using the Active Directory Domain Services Windows 2008 R2 cmdlets, see Hey, Scripting Guy! What’s Up with Active Directory Domain Services Cmdlets?
At some point, I probably will add support for inputting alternative credentials and support for other domains. But for now, it is a single command:
Search-ADAccount -AccountDisabled -UsersOnly | Format-Table name, sid -AutoSize
When the Search-ADAccount command runs, the output appears that is shown in the following image.
From a performance standpoint, which of the three different approaches is best? Let us begin with the WMI Win32_UserAccount script.
See this link for a collection of Hey, Scripting Guy! articles that talk about testing the performance of various Windows PowerShell scripts.
When I use the Measure-Command cmdlet, it takes 571 milliseconds to return. Keep in mind that my AD is small, and I have two domain controllers on my switched gigabyte network.
Windows PowerShell
Copyright (C) 2010 Microsoft Corporation. All rights reserved.
PS C:\> measure-command { C:\data\Windows2008ResKit\WS08SecurityResKit\LocateDisabled
Users.ps1 -domain nwtraders -q }
15 disabled in the nwtraders domain — List follows:
====================================================
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 571
Ticks : 5719349
TotalDays : 6.61961689814815E-06
TotalHours : 0.000158870805555556
TotalMinutes : 0.00953224833333333
TotalSeconds : 0.5719349
TotalMilliseconds : 571.9349
PS C:\>
I then rebooted my workstation and ran the ADSISearcher script. The reason for rebooting my workstation is to avoid any kind of false performance boosts from ADSI caching. When the script runs, it takes 690 milliseconds to complete. One of the things to keep in mind is that the FindDisabledUserAccounts.ps1 script writes to the Windows PowerShell console, which will increase the amount of time to run the script.
My workstation has solid-state hard drives, and therefore boots really quickly. In fact, if I turn on my workstation and my laptop at the same time, I can be logged into the domain and have Word started before I am even prompted to type in the Windows BitLocker Drive Encryption PIN for my laptop (a prompt that comes up before the Windows 7 even begins to load). To be perfectly honest, if I were to do the testing on my laptop, I probably would forego the additional reboots.
PS C:\> Measure-Command { C:\data\ScriptingGuys\2010\HSG_8_16_10\FindDisabledUserAcco
unts.ps1 }
account is disabled
account is disabled
account is not disabled
account is not disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is disabled
account is not disabled
account is not disabled
account is not disabled
account is not disabled
account is not disabled
account is not disabled
account is not disabled
account is not disabled
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 690
Ticks : 6904643
TotalDays : 7.9914849537037E-06
TotalHours : 0.000191795638888889
TotalMinutes : 0.0115077383333333
TotalSeconds : 0.6904643
TotalMilliseconds : 690.4643
PS C:\>
I reboot my workstation one more time, and after loading the Active Directory module, I run the Search-ADAccount cmdlet command and use the Measure-Command cmdlet to time the operation. The results show that it took 81 milliseconds to complete, making it the fastest of the three commands:
Windows PowerShell
Copyright (C) 2010 Microsoft Corporation. All rights reserved.
PS C:\> Import-Module ac*
PS C:\> measure-command{Search-ADAccount -AccountDisabled -UsersOnly | Format-Table
name, sid -AutoSize }
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 81
Ticks : 819660
TotalDays : 9.48680555555556E-07
TotalHours : 2.27683333333333E-05
TotalMinutes : 0.0013661
TotalSeconds : 0.081966
TotalMilliseconds : 81.966
What does all this tell me? For my environment, any of the three approaches work. And all are virtually the same speed. Keep in mind when using the Measure-Command cmdlet that you should be really careful making judgments based upon milliseconds—it simply is not that accurate. If the results are several seconds apart, you have a guideline to go on. But milliseconds? Dude, the antivirus program can kick in and start a scan, or a process can decide to start. Anything can cause a time skew of a few hundred milliseconds.
In addition, you may have other requirements that will dictate which approach you use. The following table summarizes the three different techniques.
Approach |
Use |
WMI Win32_UserAccount |
VBScript, Windows PowerShell 1.0, Windows PowerShell 2.0. If VBScript, then Windows 2000 and later. If Windows PowerShell, then Windows XP and later. |
ADSISearcher |
Windows PowerShell 2.0, Windows XP, and later on client. |
Search-ADAccount |
Windows PowerShell 2.0, Windows 7 with RSAT tools installed, Windows Server 2008 R2 domain controller. |
EJ, that is all there is to using Windows PowerShell to search for disabled user accounts. User Management Week will continue tomorrow when we will talk about changing a user’s password.
We invite you follow us on Twitter and Facebook. If you have any questions, send email to us at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum.. See you tomorrow. Until then, peace.
Ed Wilson and Craig Liebendorfer, Scripting Guys
0 comments