Wednesday, September 15, 2010

Cleanup Unused User Accounts


If your environment is anything like those that I have been a part of, it is a relatively simple thing for user accounts to continue to exist long after their true usefulness is past. Most organizations are good at cleaning up accounts that actually belong to a person, once the individual has moved on for whatever reason. This is because there are usually checks and balances in place, such as a reminder from your HR staff, to make sure these accounts are properly closed out.
 If these were the only kinds of accounts in our environment, we would likely be in pretty good shape; however they are not. In most organizations there are a multitude of other sources of user accounts:
  • Application Service Accounts
  • Accounts for external user access like vendors, customers, or contractors
Earlier this week we covered cleaning up unused computer accounts from your directory. Finding these accounts is relatively simple as there are built-in mechanisms within the computer account itself to identify whether the account is being used or not. The primary means of doing this for computer accounts is to look at the date of last password change. For true user accounts, this is not very efficient as the password change frequency is typically pretty low, 90 days is not uncommon. Additionally, it is not uncommon for at least some users to be exempted from the password change process due to remote work assignment or some other reason. As a result we need to look at some other detail within the account itself to determine if it is being used or not.
All accounts do include a last logon field. This field would be ideal as it is time stamped each time the account successfully authenticates regardless of the source of this authentication, but unfortunately this field is not replicated to every domain controller in the environment. Each domain controller keeps their own copy of this field, so to determine the last logon one needs to search every domain controller and determine which has the most recent logon date.
Usage is straightforward. Assuming you save the script contents as uaarr2k.vbs, the command line syntax is:
Cscript uaarr2k.vbs [-DAYS <n>] [-DISABLE]
-DAYS <n>Display computer accounts that have not reset their domain password within n days. Note: n must be an integer, and must be at least 1.
-DISABLEDisable computer accounts that have not reset their domain password within n days. Note: If this switch is used, n must be at least 30
-?, -HDisplays this help
   
In normal use, it is recommended to run this script as 2 separate steps. A first step would be to search the domain for all user accounts that have not successfully authenticated in the prescribed length of time. This step should be run and its output captured such that it can be reviewed manually by the internal team to make sure there are not any account that would be disabled unnecessarily. When performing this step, make sure that the length of time specified for the scan is at least 1.5X the length of your account password expiration setting in AD. This can also be controlled via GPO. If you wish to run the script to look for accounts older than 60 days and save them to an output file named c:\review.txt, the command line would be:
Cscript uaarr2k.vbs –days 60 > c:\review.txt
This will provide a semicolon delimited file that can be manually reviewed. If this produces a potential change set that requires no changes, you can run the script a second time with the –disable switch. This will disable all accounts meeting the date requirements.
Hopefully, this script provides another good starting point for directory cleanup.
Script contents below:
Option Explicit


' User Account Age Reporter And Remover
' This script will report on user accounts meeting the specified date
' parameters. With the /disable switch specified, the script will disable
' user accounts that meet the date requirements. The script will not allow
' accounts to be disabled that have been logged into in the last 30 days.
'
' Author: Tom Owen
' Original Source: Cwashington.netreach.net & www.developersdex.com
' Date: 7/1/2004




Sub Usage()
WScript.Echo UCase(WScript.ScriptName) &_
" - User Account Age Reporter And Remover" & vbNewLine & vbNewLine &_
"Enumerates computer accounts and displays number of days since" & vbNewLine &_
"last update of the computer account password." & vbNewLine & vbNewLine &_
WScript.ScriptName & " [-DAYS <n>] [-DISABLE]" & vbNewLine &_
"-DAYS <n> Show only those computers who have not connected in n days" & vbNewLine &_
"-DISABLE Disable computer accounts meeting the number of days requirement" & vbNewLine &_
"-?, -H Displays this help" & vbNewLine & vbNewLine
WScript.Quit
End Sub


Dim objRootDSE, strConfig, objConnection, objCommand, strQuery
Dim objRecordSet, objDC
Dim strDNSDomain, objShell, lngBiasKey, lngBias, k, arrstrDCs()
Dim strDN, dtmDate, objDate, lngDate, objList, strUser
Dim strBase, strFilter, strAttributes, lngHigh, lngLow
Dim i, numdays, boolDisable, dtmNow, objAccount


i = 0
numdays=-1
boolDisable = false
While i < WScript.Arguments.Count
Select Case Replace(UCase(WScript.Arguments.Item(i)),"/","-")
Case "-DAYS":
i=i+1
numdays=clng(WScript.Arguments.Item(i))
Case "-DISABLE":
boolDisable = true
Case "-?":
usage()
Case else:
usage()
End Select
i = i + 1
wend


' Check to make sure that the numdays specified is more than 28 days
if boolDisable = true and numdays <= 30 then
wscript.echo "ERROR: " & UCase(WScript.ScriptName) & _
" - Computer Account Age Reporter And Remover" & vbNewLine & vbNewLine &_
"cannot disable accounts newer than 30 days" & vbNewLine
wscript.quit
end if


' Use a dictionary object to track latest lastLogon for each user.
Set objList = CreateObject("Scripting.Dictionary")
objList.CompareMode = vbTextCompare


' Obtain local Time Zone bias from machine registry.
Set objShell = CreateObject("Wscript.Shell")
lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _
& "TimeZoneInformation\ActiveTimeBias")
If UCase(TypeName(lngBiasKey)) = "LONG" Then
lngBias = lngBiasKey
ElseIf UCase(TypeName(lngBiasKey)) = "VARIANT()" Then
lngBias = 0
For k = 0 To UBound(lngBiasKey)
lngBias = lngBias + (lngBiasKey(k) * 256^k)
Next
End If


' Determine configuration context and DNS domain from RootDSE object.
Set objRootDSE = GetObject("LDAP://RootDSE")
strConfig = objRootDSE.Get("configurationNamingContext")
strDNSDomain = objRootDSE.Get("defaultNamingContext")


' Use ADO to search Active Directory for ObjectClass nTDSDSA.
' This will identify all Domain Controllers.
Set objCommand = CreateObject("ADODB.Command")
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
objCommand.ActiveConnection = objConnection


strBase = "<LDAP://" & strConfig & ">"
strFilter = "(objectClass=nTDSDSA)"
strAttributes = "AdsPath"
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"


objCommand.CommandText = strQuery
objCommand.Properties("Page Size") = 100
objCommand.Properties("Timeout") = 60
objCommand.Properties("Cache Results") = False


Set objRecordSet = objCommand.Execute


' Enumerate parent objects of class nTDSDSA. Save Domain Controller
' AdsPaths in dynamic array arrstrDCs.
k = 0
Do Until objRecordSet.EOF
Set objDC = _
GetObject(GetObject(objRecordSet.Fields("AdsPath")).Parent)
ReDim Preserve arrstrDCs(k)
arrstrDCs(k) = objDC.DNSHostName
k = k + 1
objRecordSet.MoveNext
Loop


' Retrieve lastLogon attribute for each user on each Domain Controller.
For k = 0 To Ubound(arrstrDCs)
strBase = "<LDAP://" & arrstrDCs(k) & "/" & strDNSDomain & ">"
strFilter = "(&(objectCategory=person)(objectClass=user))"
strAttributes = "distinguishedName,lastLogon"
strQuery = strBase & ";" & strFilter & ";" & strAttributes _
& ";subtree"
objCommand.CommandText = strQuery
On Error Resume Next
Set objRecordSet = objCommand.Execute
If Err.Number <> 0 Then
On Error GoTo 0
Wscript.Echo "Domain Controller not available: " & arrstrDCs(k)
Else
On Error GoTo 0
Do Until objRecordSet.EOF
strDN = objRecordSet.Fields("distinguishedName")
lngDate = objRecordSet.Fields("lastLogon")
On Error Resume Next
Set objDate = lngDate
If Err.Number <> 0 Then
On Error GoTo 0
dtmDate = #1/1/1601#
Else
On Error GoTo 0
lngHigh = objDate.HighPart
lngLow = objDate.LowPart
If lngLow < 0 Then
lngHigh = lngHigh + 1
End If
If (lngHigh = 0) And (lngLow = 0 ) Then
dtmDate = #1/1/1601#
Else
dtmDate = #1/1/1601# + (((lngHigh * (2 ^ 32)) _
+ lngLow)/600000000 - lngBias)/1440
End If
End If
If objList.Exists(strDN) Then
If dtmDate > objList(strDN) Then
objList(strDN) = dtmDate
End If
Else
objList.Add strDN, dtmDate
End If
objRecordSet.MoveNext
Loop
End If
Next


'Get the current Time for comparison
dtmNow = Now()


' Output latest lastLogon date for each user.
For Each strUser In objList


' If a number of days has been specified
if numdays > 0 then
if boolDisable = false then
'report Only option has been selected
if datediff("D", objList(strUser), dtmNow) > numdays then
Wscript.Echo strUser & " ; " & objList(strUser)
end if
else
' Disable option has been specified.
if datediff("D", objList(strUser), dtmNow) > numdays then
err.clear
set objAccount = GetObject("LDAP://" & strUser)
if err.number <> 0 then
Wscript.Echo strUser & " ; " & objList(strUser) & " ; Unable to Connect"
err.clear
else
objAccount.AccountDisabled = True
if err.number <> 0 then
Wscript.Echo strUser & " ; " & objList(strUser) & " ; Unable set disabled flag"
err.clear
else
objAccount.setinfo
if err.number <> 0 then
Wscript.Echo strUser & " ; " & objList(strUser) & " ; Unable to Disable"
err.clear
else
Wscript.Echo strUser & " ; " & objList(strUser) & " ; Disabled"
end if
end if
end if
set objAccount = Nothing
end if
end if
else
'report all accounts
Wscript.Echo strUser & " ; " & objList(strUser)
end if
Next


' Clean up.
objConnection.Close
Set objRootDSE = Nothing
Set objConnection = Nothing
Set objCommand = Nothing
Set objRecordSet = Nothing
Set objDC = Nothing
Set objDate = Nothing
Set objList = Nothing
Set objShell = Nothing

No comments:

Post a Comment