Executive Summary:

StaleAccounts.js, a new tool written in JScript, makes finding stale accounts in Active Directory (AD) easy. It searches using the lastLogon, lastLogonTimestamp, or pwdLastSet attributes, and it can look for either user or computer objects.

Have you ever wished you had a list of every stale account in a domain? Such a list would help you deal more efficiently with accounts that haven’t seen activity lately—either the password hasn’t been changed in a while or it hasn’t been used to log on recently. The LastLogon.js script that I presented in "Finding a User's Last Logon" (July 2007, InstantDoc ID 96302), lists an account's last logon date and logon server. Although LastLogon.js is useful, I wanted a more specific tool that could find stale accounts. So I wrote StaleAccounts.js. It searches Active Directory (AD) for stale accounts based on the number of days of inactivity that you specify on the command line. It uses the lastLogon, lastLogonTimestamp, or pwdLastSet attribute for searching, and it can search for either user or computer objects.

Using StaleAccounts.js
StaleAccounts.js uses the following command-line syntax:

\[cscript\] StaleAccounts.js
  \[/lastlogon | /lastlogontimestamp | /pwdlastset\]
  /days:days \[location\] \[/norecurse\] \[/expiredpwds\]
  \[/computers\]  \[/d\[:char\]\]

StaleAccounts.js requires the CScript host, so the cscript keyword at the beginning of the command line is required only if CScript isn't your default script host. I recommend setting CScript as your default script host. To do so, use the following command:

cscript //h:cscript //nologo //s

StaleAccounts.js can search for stale accounts in three ways, as listed in Table 1. You must specify either /lastlogon, /lastlogontimestamp, or /pwdlastset.

The /days parameter specifies the number of days of inactivity for accounts. The number of days means different things depending on whether you use /lastlogon, /lastlogontimestamp, or /pwdlastset. For example, /lastlogon /days: 42 means "only list accounts that have not been used to log on in the last 42 days."

The location parameter specifies the the accounts’ AD location (typically, this is the distinguished name of an OU). If the string contains spaces, enclose it in quotes (for example, "OU=Sales Team,DC=wascorp,DC=net"). The location parameter is optional; if you omit it, StaleAccounts.js searches the entire domain. If you want to limit the search to the specified container, enter the /norecurse parameter; otherwise, the script searches in the specified container as well as all containers underneath it.

The /expiredpwds parameter instructs StaleAccounts.js to list accounts that are required to change their passwords at the next logon (i.e., it finds accounts whose pwdLastSet attribute equals zero). You can also use this parameter to search for accounts that have never been used to log on.

The /computers parameter searches for computer objects instead of user objects. This is useful for finding computers that haven't been used for a period of time.

Finally, the /d parameter enables delimited output. Without a delimiter character (i.e., /d by itself), StaleAccounts.js uses a tab. If you specify a character, the script will use it instead. For example, /d:, will cause the script to generate comma-delimited output. Without the /d parameter, the script will output its data in a list-style format that I'll describe shortly.

Suppose your domain's maximum password age is 60 days, and you want to see how many passwords have not been set for at least this long. Use the following command:

StaleAccounts.js /pwdlastset /days:60

Figure 1 shows the kind of output StaleAccounts.js generates when you use the /pwdlastset parameter. The output lists the account's distinguished name (DN), the logon name (AD attribute sAMAccountName), whether the account's password is allowed to expire, and when the password was last set for the account.

If you want to generate comma-delimited output instead, use a command like the following:

StaleAccounts.js /pwdlastset /days:60 /d:, >  StaleAccounts.csv

This outputs the list of accounts to the file StaleAccounts.csv. You can then use CSVviewer.hta (see "Display CSV Files in an HTA," January 2007, InstantDoc ID 94260) to view and sort the output, or you can import it into a spreadsheet. Note that StaleAccounts.js searches only for enabled accounts, as you can't log on using a disabled account.

Inside StaleAccounts.js
At the top of the script, StaleAccounts.js declares some global values in uppercase characters that represent constants for the entire script. It also declares the g_DCs variable as a global variable. This variable will contain an array of domain controllers (DCs) if the /lastlogon option is used. Next, the script modifies the Boolean object's toString method to return yes or no rather than true or false. The script also adds the hex method to the Number object to return the number as a hexadecimal string. After this, the script defines the constructor function for the Account object, which is used later in the script. After defining the Account object, the script executes the main function by calling it as a parameter to the WScript.Quit method (i.e., the main function's exit code will be the script's exit code).

The main Function
The main function performs five tasks:

  1. It parses the command line and validates the command-line parameters.
  2. It determines the base DN for the search.
  3. It retrieves a list of the names of the DCs in the domain.
  4. It creates a Date object containing the specified number of days in the past.
  5. It calls the listStaleAccounts function using the parameters gathered in the previous steps.

Task 1: Parse the command line and validate the command-line parameters.The main function uses the WshNamed and WshUnnamed objects to retrieve the script's command-line parameters. The WshNamed object contains the script's named parameters (the parameters that start with a forward slash), and the WshUnnamed object contains the script's unnamed parameters (the parameters that don't start with a forward slash). The main function uses both the WshNamed and WshUnnamed objects to retrieve the data from the command line.

Task 2: Determine the base DN for the search. The base DN, which is the starting point for the search, can come from the first unnamed argument, or you can retrieve the domain's default naming context (e.g., DC=wascorp,DC=net). If the command line contains a DN, then the main function uses the getDomainDN function to return the domain name portion of the DN. For example, if the command line specifies the base DN as OU=Sales,DC=wascorp,DC=net, then the getDomainDN function returns DC=wascorp,DC=net (i.e., only the domain name portion).

Task 3: Retrieve a list of DC names in the domain. If the command line contains the /lastlogon parameter, the main function calls the getDCs function to retrieve the domain's DCs and stores this array in the g_DCs variable.

Task 4: Create a Date object containing the specified number of days in the past. Next, to create a Date object containing n days ago (where n is the argument to the /days parameter on the command line), the main function uses the following formula:

Today - n days ago

Task 5: Call the listStaleAccounts function by using the parameters gathered in the previous steps. The listStaleAccounts function uses the information retrieved by the main function to search AD and list matching accounts.

The listStaleAccounts Function
The listStaleAccounts function uses ActiveX Data Objects (ADO) to search AD and list matching accounts. In Listing 1, you can see an excerpt from the function that shows how it creates and configures the ADO objects. This excerpt shows how the function sets the CommandText property of the ADODB.Command object to the LDAP query. The ? character is part of JScript's ternary (three-part) operator. Here’s how the ternary operator works: The ? operator evaluates the expression to its left. If the expression evaluates to true, it returns the expression to the left of the ? operator; otherwise, it returns the expression to the right of the : operator. For example, consider the following expression:

(recurse ? "subtree" : "onelevel")

If the recurse variable (a Boolean value) is true, this expression returns the string "subtree"; otherwise, it returns the string "onelevel". The ternary operator is a convenient way of building the LDAP query string without the need for a group of messy if statements.

Next, the listStaleAccounts function outputs a header line if a delimiter character is defined. After this, it calls the ADODB.Command object's Execute method. If the Execute method is successful, it returns a Recordset object containing the results of the query. If the method fails, a runtime error will occur, so the listStaleAccounts function uses a try...catch block to handle the error gracefully.

The listStaleAccounts now has a RecordSet object containing AD account objects (either users or computers, depending on whether the /computers parameter was specified on the command line). The function then reads the account's userAccountControl attribute to determine whether the password on the account expires. Next, it creates an empty result object that will contain the data retrieved from the getLastLogon, getLastLogonTimestamp, or get pwdLastSet function. The listStaleAccounts function then uses the switch statement to decide which of the three functions to use.

If the date returned in the result object from one of the get... functions is old enough, the listStaleAccounts function creates an Account object (the Account object is defined at the top of the script). The function then calls the outputLastLogon, outputLastLogonTimestamp, or outputPwdLastSet function to output the account data to standard output.

The getLastLogon Function
To connect to each DC and retrieve the most recent logon for the account, the getLastLogon function uses the array of DCs stored in the g_DCs variable. The getLastLogon function uses the LastLogin attribute rather than lastLogon because LastLogin is a VT_DATE (a VBScript date value) rather than a 64-bit large integer value. AD provides the LastLogin attribute as an alternative to lastLogon so script authors don't have to perform a 64-bit to 32-bit conversion. (I describe the conversion process when I discuss the getLastLogonTimestamp function later.) The getLastLogon function reads the LastLogin attribute within a try...catch block in case the attribute doesn't exist. If the function fails to find a most-recent logon, it returns a zero date. ("Zero" means midnight, January 1, 1970 UTC; I use the term "zero" because that's what the getTime method returns.) Otherwise, the function updates the properties of the result object with the name of the server that authenticated the logon and the latest logon time.

The getLastLogonTimestamp Function
The getLastLogonTimestamp function, shown in Listing 2, connects to the AD account and uses the AD account's Get method to retrieve its lastLogonTimestamp attribute. The lastLogonTimestamp attribute, like the lastLogon attribute, is stored in AD as an IADsLargeInteger object that contains the number of 100-nanosecond intervals since midnight, January 1, 1601 UTC. An IADsLargeInteger object has two properties, HighPart and LowPart, that contain the high-order and low-order 32-bits, respectively, of the 64-bit value. The function retrieves the HighPart and LowPart values and then uses the following formula to copy the 64-bit value into a 32-bit value:

(HighPart * 232) + LowPart

The high-order and low-order portions are unsigned integers which JScript might interpret as negative numbers (if they're large enough), so the getLastLogonTimestamp function uses the toUnsigned function, shown in Listing 3, on both the high-order and low-order portions of the 64-bit value to convert them to unsigned values before using the above formula. After the conversion, the function constructs a new Date object using the number of 100-nanosecond intervals it calculated using the UTC method of the Date object.

There is an important caveat, however. Because a 64-bit value is double the size of a 32-bit value, there may be a loss of precision when the getLastLogonTimestamp function attempts to convert the 64 bits into 32 bits. If the number is big enough, some of the bits at the end of the number will get truncated, so the result may be approximate for larger values. Unfortunately, Microsoft has not provided a VT_DATE equivalent for the lastLogonTimestamp attribute (like LastLogin), so you'll have to be content with the approximation if you want to use lastLogonTimestamp. In my experience, depending on how large the values are, this approximation may be several hours off.

The getPwdLastSet Function
The getPwdLastSet function is the fastest function of the three get... functions because it only retrieves the PasswordLastChanged attribute for the account. PasswordLastChanged is a VT_DATE equivalent of the pwdLastSet attribute (as LastLogin is for lastLogon). If the property doesn't exist, the function returns a zero date (midnight, January 1, 1970 UTC).

The Output Functions
The outputLastLogon, outputLastLogonTimestamp, and pwdLastSet functions are nearly identical except for the data they display. They output the data from the Account object created in the listStaleAccounts function. All of the output functions use the formatDate function to output the date in the following format: yyyy/mm/dd hh:mm:ss. If the Date object is a zero date (midnight, January 1, 1970 UTC), the formatDate function returns the string "N/A".

Keep Track of Account Usage
StaleAccounts.js is a powerful script that gives you the choice of three different AD attributes to find stale accounts in your domain. Run it periodically and stay informed about unused accounts on your network.