Recipe 8.8 Finding Inactive or Unused Computers

8.8.1 Problem

You want to find inactive computer accounts in a domain.

8.8.2 Solution

These solutions only apply to Windows-based machines. Other types of machines (e.g., Unix) that have accounts in Active Directory may not update their login timestamps or passwords, which are used to determine inactivity. Using a command-line interface

The following query will locate all inactive computers in the current forest:

> dsquery computer forestroot -inactive <NumWeeks>

You can also use domainroot in combination with the -d option to query a specific domain:

> dsquery computer domainroot -d <DomainName> -inactive <NumWeeks>

or you can target your query at a specific container:

> dsquery computer ou=MyComputers,dc=rallencorp,dc=com -inactive <NumWeeks>

This can only be run against a Windows Server 2003 domain functional level or higher domain. Using Perl

# Script Configuration
# Domain and container/OU to check for inactive computer accounts
my $domain        = '';

# set to empty string to query entire domain
my $computer_cont = 'cn=Computers,'; 

# Number of weeks used to find inactive computers
my $weeks_ago = 30;
# End Configuration

use strict;
use Win32::OLE;
   $Win32::OLE::Warn = 3;
use Math::BigInt;

# Must convert the number of seconds since $weeks_ago
# to a large integer for comparison against lastLogonTimestamp
my $sixmonth_secs = time - 60*60*24*7*$weeks_ago;
my $intObj = Math::BigInt->new($sixmonth_secs);
   $intObj = Math::BigInt->new($intObj->bmul('10 000 000'));
my $sixmonth_int = Math::BigInt->new(
                        $intObj->badd('116 444 736 000 000 000'));
   $sixmonth_int =~ s/^[+-]//;

# Setup the ADO connections
my $connObj                         = Win32::OLE->new('ADODB.Connection');
$connObj->{Provider}                = "ADsDSOObject";
my $commObj                         = Win32::OLE->new('ADODB.Command');
$commObj->{ActiveConnection}        = $connObj;
$commObj->Properties->{'Page Size'} = 1000;

# Grab the default root domain name
my $rootDSE = Win32::OLE->GetObject("LDAP://$domain/RootDSE");
my $rootNC = $rootDSE->Get("defaultNamingContext");

# Run ADO query and print results
my $query  = "<LDAP://$domain/$computer_cont$rootNC>;";
$query .=  "(&(objectclass=computer)";
$query .=    "(objectcategory=computer)";
$query .=    "(lastlogontimestamp<=$sixmonth_int));";
$query .=  "cn,distinguishedName;";
$query .= "subtree";
$commObj->{CommandText} = $query;
my $resObj = $commObj->Execute($query);
die "Could not query $domain: ",$Win32::OLE::LastError,"\n" 
  unless ref $resObj;

print "\nComputers that have been inactive for $weeks_ago weeks or more:\n";
my $total = 0;
while (!($resObj->EOF)) {
   my $cn  = $resObj->Fields(0)->value;
   print "\t",$resObj->Fields("distinguishedName")->value,"\n";
print "Total: $total\n";

8.8.3 Discussion Using a command-line interface

The dsquery computer command is very handy for finding inactive computers that have not logged into the domain for a number of weeks or months. You can pipe the results of the query to dsrm if you want to remove the inactive computer objects from Active Directory in a single command. Here is an example that would delete all computers in the current domain that have been inactive for 12 weeks or longer:

> for /F "usebackq" %i in (`dsquery computer domainroot -inactive 12`) do dsrm %i

Unless you have a requirement for quickly removing unused computer objects, I'd recommend allowing them to remain inactive for at least three months before removing them. If you don't really care when the objects get removed, use a year (i.e., 52 weeks) to be on the safe side. Using Perl

With Windows 2000 Active Directory, the only way you can determine if a computer is inactive is to query either the pwdLastSet or lastLogon attributes. The pwdLastSet attribute is a 64-bit integer that translates into the date and time the computer last updated its password. Since computers are suppose to change their password every 30 days, you could run a query that finds the computers that have not changed their password in several months. This is difficult with VBScript because it does not handle 64-bit integer manipulation very well. There are third-party add-ons you can get that provide 64-bit functions, but none of the built-in VBScript functions can do it and it is non-trivial to implement without an add-on.

The lastLogin attribute can also be used to find inactive computers because that attribute contains a 64-bit integer representing the last time the computer logged into the domain. The problem with the lastLogin attribute is that it is not replicated. Since it is not replicated, you have to query every domain controller in the domain to find the most recent lastLogin value. As you can imagine, this is less than ideal, especially if you have a lot of domain controllers.

Fortunately, in Windows Server 2003, Microsoft added a new attribute called lastLogonTimestamp to user and computer objects. This attribute contains the approximate last logon timestamp (again in a 64-bit, large-integer format) for the user or computer and is replicated to all domain controllers. It is the "approximate" last logon because the domain controllers will update the value only if it hasn't been updated for a certain period of time (such as a week). This prevents the attribute from being updated constantly and causing a lot of unnecessary replication traffic.

Since VBScript was out of the question, I turned to my first love . . . Perl. It is very rare to find a problem that you can't solve with Perl and this is no exception. The biggest issue is manipulating a number to a 64-bit integer, which we can do with the Math::BigInt module.

First, I determine the time in seconds from 1970 for the date that we want to query computer inactivity against. That is, take the current time and subtract the number of weeks we want to go back. Then I have to convert that number to a big integer. The last step is simply to perform an ADO query for all computers that have a lastLogonTimestamp less than or equal to the value I just calculated.

8.8.4 See Also

Recipe 6.26 for finding users whose accounts are about to expire

    Chapter 3. Domain Controllers, Global Catalogs, and FSMOs
    Chapter 6. Users
    Appendix A. Tool List