4 Challenges of Auditing AD Users and Groups
This Windows PowerShell script makes fast work of a tricky task
February 22, 2012
I've lost track of the number of times that someone has asked in an online forum: "Does someone know how to list all users and their group membershipsin an Active Directory domain?" Third-party auditors or security consultants also ask this question when trying to assess an organization's ActiveDirectory (AD) environment. Because the question is so common, I decided to write a Windows PowerShell script to address the task.
I initially thought that writing such a script would be simple, but four challenges caused the task to take a little longer than I first expected. I'lldescribe these issues, but first I need to explain a bit about the basics of using Microsoft .NET in PowerShell to search AD. (I won't discussPowerShell's ActiveDirectory module here, as I like to maintain backward compatibility with earlier OS versions and domains.)
Using .NET to Search AD
When using .NET to search AD, you can use the [ADSISearcher] type accelerator in PowerShell to search for objects. (A type accelerator is a shortcutname for a .NET class.) For example, enter the following commands at a PowerShell prompt to output a list of all users in the current domain:
PS C:> $searcher = [ADSISearcher] "(&(objectCategory=user)(objectClass=user))"PS C:> $searcher.FindAll()
[ ADSISearcher] is a type accelerator for the .NET System.DirectoryServices.DirectorySearcher object. The string following this type accelerator setsthe object's SearchFilter property to find all user objects, and the FindAll method starts the search. The output is a list ofSystem.DirectoryServices.SearchResult objects.
So far, so good. Next, we want to determine a user's group memberships. To do so, we can use the Properties collection from a SearchResult object andretrieve the object's memberof attribute. Using the $searcher variable from the previous example, we can use the FindOne method (instead of FindAll) toretrieve one result and output the user's group memberships:
PS C:> $result = $searcher.FindOne()PS C:> $result.Properties["memberof"] | sort-object
The first command finds the first user that matches the search filter, and the second command outputs a list of the groups of which that user is amember.
However, if you look carefully at this list, you'll notice that something is missing: The user's primary group is not included in the memberofattribute. I wanted the complete list of groups (including the primary group) -- which leads us to the first challenge.
Challenge #1: Finding a User's Primary Group
There is a workaround for the exclusion of the primary group from the memberof attribute. This workaround is described in the Microsoft article "How toUse Native ADSI Components to Find the Primary Group." The workaround uses these steps:
1. Connect to the user object by using the WinNT provider (instead of the LDAP provider).
2. Retrieve the user's primaryGroupID attribute.
3. Retrieve the names of the user's groups by using the WinNT provider, which includes the primary group.
4. Search AD for these groups by using their sAMAccountName attributes.
5. Find the group in which the primaryGroupToken attribute matches the user's primaryGroupID.
The problem with this workaround is that it requires the script to use the WinNT provider to connect to the user object. That is, I needed the scriptto translate a user's distinguished name (DN; e.g., CN=Ken Myer,OU=Marketing,DC=fabrikam,DC=com) into a format that the WinNT provider could use (e.g.,WinNT://FABRIKAM/kenmyer,User).
Challenge #2: Translating Between Naming Formats
The NameTranslate object is a COM (ActiveX) object that implements the IADsNameTranslate interface, which translates the names of AD objects intoalternate formats. You can use the NameTranslate object by creating the object and then calling its Init method to initialize it. For example, Listing1 shows VBScript code that creates and initializes the NameTranslate object.
However, the NameTranslate object does not work as expected in PowerShell, as Figure 1 shows. The problem is that the NameTranslate object does nothave a type library, which .NET (and thus PowerShell) uses to provide easier access to COM objects. Fortunately, there is a workaround for this problemas well: The .NET InvokeMember method allows PowerShell to get or set a property or execute a method from a COM object that's missing a type library.Listing 2 shows the PowerShell equivalent of the VBScript code in Listing 1.
Figure 1: Unexpected behavior of the NameTranslate object in PowerShell
I wanted the script to handle one other name-related problem. The memberof attribute for an AD user contains a list of DNs of which a user is a member,but I wanted the samaccountname attribute for each group instead. (This is called the Pre-Windows 2000 name in the Active Directory Users and ComputersGUI.) The script uses the NameTranslate object to handle this issue also.
Challenge #3: Dealing with Special Characters
The Microsoft documentation regarding DNs mentions that certain characters must be "escaped" (i.e., prefixed with a character) to be interpretedproperly (see the Microsoft article "Distinguished Names" for more information). Fortunately, the Pathname COM object provides this capability. Thescript uses the Pathname object to escape DNs that contain special characters. The Pathname object also requires the .NET InvokeMember method because,like the NameTranslate object, this object lacks a type library.
Challenge #4: Improving Performance
If you look back at Challenge #1 (Finding a User's Primary Group), you'll notice that the workaround solution requires an AD search for a user'sgroups. If you repeat this search for many user accounts, the repeated searching adds up to a lot of overhead. Retrieving the samaccountname attributefor each group in the memberof attribute that I mentioned in Problem #2 (Translating Between Naming Formats) also adds overhead. To meet thischallenge, the script uses two global hash tables that cache results for improved performance.
Get-UsersAndGroups.ps1Get-UsersAndGroups.ps1 is the completed PowerShell script, which generates a list of users and the users' group memberships. The script's command-line syntax is as follows:Get-UsersAndGroups [[-SearchLocation] ] [-SearchScope ]
The -SearchLocation parameter is one or more DNs to search for user accounts. Because a DN contains commas (,), enclose each DN in single quotes (') ordouble quotes (") to prevent PowerShell from interpreting it as an array. The -SearchLocation parameter name is optional. The script also acceptspipeline input; each value from the pipeline must be a DN to search.
The -SearchScope value specifies the possible scope for the AD search. This value must be one of three choices: Base (limit the search to the baseobject, not used), OneLevel (search the immediate child objects of the base object), or Subtree (search the entire subtree). If no value is specified,the Subtree value is used by default. Use -SearchScope OneLevel if you want to search a particular organizational unit (OU) but none of the OUs underit. The script outputs objects that contain the properties that are listed in Table 1.
Overcoming the 4 Challenges
The script implements the solutions to the four challenges that I mentioned earlier:
Challenge #1 (Finding a User's Primary Group): The get-primarygroupname function returns the primary group name for a user.
Challenge #2 (Translating Between Naming Formats): The script uses the NameTranslate COM object to translate between naming formats, asdiscussed previously.
Challenge #3 (Dealing with Special Characters): The script uses the get-escaped function, which uses the Pathname object to return DNs with theproper escape characters inserted where needed.
Challenge #4 (Improving Performance): The script uses the $PrimaryGroups and $Groups hash tables. The $PrimaryGroups hash table's keys areprimary group IDs, and its values are the primary groups' samaccountname attributes. The $Groups hash table's keys are the groups' DNs, and its valuesare the groups' samaccountname attributes.
User and Group Auditing Made Easy
Writing the Get-UsersAndGroups.ps1 script wasn't as straightforward as I thought it would be, but using the script couldn't be easier. The simplestapplication of the script is a command such as the following:
PS C:> Get-UsersAndGroups | Export-CSV Report.csv -NoTypeInformation
This command creates a comma-separated value (CSV) file that contains a complete list of users and groups for the current domain. With this script inyour toolkit, you can effortlessly create that users-and-groups report that your organization needs, in record time.
Listing 1: Creating and Initializing the NameTranslate Object in VBScript
Const ADS_NAME_INITTYPE_GC = 3Dim NameTranslateSet NameTranslate = CreateObject("NameTranslate")NameTranslate.Init ADS_NAME_INITTYPE_GC, vbNull
Listing 2: Creating and Initializing the NameTranslate Object in PowerShell
$ADS_NAME_INITTYPE_GC = 3$NameTranslate = new-object -comobject NameTranslate[Void] $NameTranslate.GetType().InvokeMember("Init", "InvokeMethod", $NULL, $NameTranslate, ($ADS_NAME_INITTYPE_GC, $NULL))
About the Author
You May Also Like