Find Old Computer Accounts in Active Directory

A Script to Find Old Computers in Your Active Directory:  Many companies find their Active Directory filling up with junk over time. As laptops, desktops and servers are built, rebuilt, renamed, and retired, unless you reuse the same computer names or actively delete old computer objects are part of your daily process, you end up with lots of old, inactive computer accounts. In a large organization, it can be difficult to identify which accounts are active and which are not.

Maybe you don't care if there's old computer accounts lying around. They don't usually cause a great deal of harm, unless you are using the list of computer accounts in Active Directory to perform maintenance tasks against all those computers. Microsoft Systems Management Server (SMS) for example, may use the computer accounts in a domain or OU as a collection. It may try to deploy a security patch to all of the computers in the domain. If you have a lot of inactive computer accounts, SMS will waste lots of time trying to deploy the patch to computers that don't really exist anymore, thereby slowing the deployment process down to a crawl. So much for your agile security response.


OK, you're convinced now, you want to get rid of your old computer accounts. So, short of walking the floor with your clipboard and writing down every computer name, how can you tell if a computer account is inactive?

There is the pwdLastSet attribute, which stores the time when the computer account password was last set. Yes, computers have their own password, and computers that are members of a domain periodically change their password. No humans know the password, the computer sets its password on its own. The computer typically changes its password every two weeks (assuming that it's connected to the network during that time).

The pwdLastSet attribute is stored as a large integer object, that stores a really big number, the number of 100 nanosecond intervals since midnight on January 1st 1601.

Huh?

Why Microsoft decided to store these dates in this format is hard to say, but it has to do with the Gregorian calendar, which started around 1600, so for sure, you can express just about any date and time on the Gregorian calendar with high precision using this format.

Anyway, when you retrieve one of these objects from AD, you get a largeInt object that has two attributes, a highPart (high order bits) and a lowPart (low order bits). You then have to multiply the highPart by 2^32 (2 to the 32nd power) then add the low part. The result is the number of 100 nanosecond intervals since Jan 1st 1601. Then simply convert to days right? Well, that gave us the number of days from Jan 1st 1601 til the day the password was set. Not very useful until we figure out how many days from Jan 1st 1601 to today, then subtract the number of days from Jan 1st 1601 til the day the password was set. The result is the number of days from when the password was set to today, which is really what we want. Right? Right.

OK, so how many days has it been since Jan 1st 1601 til today? Well we just take this year and subtract 1601, then multiply by 365 day a year, account for the number of days in each month, etc, etc. Right? Wrong! We have to account for leap years (when there's 366 days), and they occur every four years. Right? Wrong! Ready for this? They really occur every four years, but every hundred years we skip one, but every four hundred years we don't skip one. That is, 1600 was a leap year, 1700 wasn't, 1800 wasn't, 1900 wasn't, and 2000 was.  None of us mortals noticed anything because we all expected 2000 to be a leap year, and it was.  It just happened to be the end of the 400 year cycle.  The next one is the year 2400.  Hopefully by then, Perl version 6 will have been released (yeah, you heard me Larry!), and maybe they'll put a date math module in there.  In any case, hopefully I'll be dead by then so I don't have to explain this again.  Elroy Jetson will just have to figure this out on his own.

So we have to take all this into account to figure out the actual number of leap years (x 366 days per leap year), and the number of non leap years (x 365 days per non-leap year), and the number of days so far this year, to get the number of days since Jan 1st 1601. Lucky for a lot of developers, there are functions in many languages to figure this out (like datediff in VBscript). But to demonstrate, let's do it the hard way, with Perl.

Here's the code to figure out how many days there have been since Jan 1st 1601.

#-------------------------------------------------------
#calculate the number of days from Jan 1st 1601 to today
#-------------------------------------------------------
($sec,$min,$hour,$dayOfTheMonth,$mon,$year,$dayOfTheWeek,$dayOfTheYear,$isDst) = localtime(time);
$yearsSince1601=$year+1900-1601;
$leapYears=sprintf("%.0d",$yearsSince1601/4);
$leapYearSkips=sprintf("%0.d",$yearsSince1601/100) - sprintf("%.0d",$yearsSince1601/400);
$leapYears=$leapYears-$leapYearSkips;
$nonLeapYears=$yearsSince1601-$leapYears;
$leapYearDays=$leapYears*366;
$nonLeapYearDays=$nonLeapYears*365;
$daysSinceJan1st1601=$leapYearDays+$nonLeapYearDays+$dayOfTheYear;
print "$yearsSince1601 years since 1601\n";
print "$leapYears leap years since 1601\n";
print "$leapYearSkips leap years skipped since 1601\n";
print "$daysSinceJan1st1601 days since Jan 1st 1601\n";

The code starts by using the localtime function from Perl to get the current date and time. localtime returns a list of elements of the current date and time which are pretty self explanatory. The two items we want are the current year and the current day of the year. The localtime function returns the year less 1900, so we have to add 1900 to it before we subtract 1601. So $yearsSince1601=$years+1900-1601

The next thing we do is take a rough stab at the number of leap years, which is $yearsSince1601 divided by 4. We drop the fractional part (which represents the few years since the last leap year). I do this round off using the sprintf function, which allows me to format the result and drop everything after the decimal point. OK rounding done.

Next we figure out how many 100 year intervals have passed (telling us how many leap years we skipped, sort of), then we figure out how many 400 year intervals there have been, telling us how many leap years we did NOT skip. Finally we can land on how many leap years there have actually been, and multiply out the number of days.

Thank goodness that's over with. We can now cut and paste that code into our date scripts and just move on. Now that we know how many days have passed since Jan 1st 1601, we can now compare that to the number of days stored in the AD attribute, giving us a password age.

So what remains is to search for computer accounts in the AD, retrieve the pwdLastSet attribute, extract the number of 100 nanosecond intervals since Jan 1st 1601, convert that to days, and subtract that from the number of days calculated above. If its older than a given age limit (say 90 days), then we can consider the computer account inactive.

The following code does all that. Any computer accounts whose password has not been changed in 90 days or more are written to a text file for your review.

use Win32::OLE;
open OUT,">oldComputers\n";
$ageLimit=90;
#-------------------------------------------------------
#calculate the number of days from Jan 1st 1601 to today
#-------------------------------------------------------
($sec,$min,$hour,$dayOfTheMonth,$mon,$year,$dayOfTheWeek,$dayOfTheYear,$isDst) = localtime(time);
$yearsSince1601=$year+1900-1601;
$leapYears=sprintf("%.0d",$yearsSince1601/4);
$leapYearSkips=sprintf("%0.d",$yearsSince1601/100) - sprintf("%.0d",$yearsSince1601/400);
$leapYears=$leapYears-$leapYearSkips;
$nonLeapYears=$yearsSince1601-$leapYears;
$leapYearDays=$leapYears*366;
$nonLeapYearDays=$nonLeapYears*365;
$daysSinceJan1st1601=$leapYearDays+$nonLeapYearDays+$dayOfTheYear;
#-----------------------------------------
#search for computer objects in the domain
#-----------------------------------------
$dse=Win32::OLE->GetObject("LDAP://RootDSE");
$domain=$dse->Get("DefaultNamingContext");
$adpath="LDAP://".$domain;
$base="<".$adpath.">";
$connection = Win32::OLE->new("ADODB.Connection");
$connection->{Provider} = "ADsDSOObject";
$connection->Open("ADSI Provider");
$command=Win32::OLE->new("ADODB.Command");
$command->{ActiveConnection}=$connection;
$command->{Properties}->{'Page Size'}=500;
$rs = Win32::OLE->new("ADODB.RecordSet");
$command->{CommandText}="$base;(objectCategory=Computer);cn,pwdlastset,distinguishedName;subtree";
$rs=$command->Execute;
until ($rs->EOF){
 $cn=$rs->Fields(0)->{Value};
 $pwdateobj=$rs->Fields(1)->{Value};
 $dn=$rs->Fields(2)->{Value};
#-----------------------------------------------------------------
 #calculate the number of days old the computer account password is
 #-----------------------------------------------------------------
 my $days;
 my $passwordAge;
 if($pwdateobj){
  $hundredNanoSeconds=($pwdateobj->highpart * (2**32)) + $pwdateobj->LowPart;
  $seconds=$hundredNanoSeconds * .0000001;
  $days=sprintf("%.0d",$seconds/86400);
  $passwordAge=$daysSinceJan1st1601-$days;
 }
 if( $passwordAge >= $ageLimit){
  print "$cn\t$passwordAge\n";
  print OUT "$cn\t$passwordAge\t$dn\n";
 }
$rs->MoveNext;
}
close OUT;

0 comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...