This Windows PowerShell script makes fast work of a tricky task
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 memberships in an Active Directory domain?" Third-party auditors or security consultants also ask this question when trying to assess an organization's Active Directory (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'll describe 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 discuss PowerShell'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 shortcut name 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.FindAll()
[ ADSISearcher] is a type accelerator for the .NET System.DirectoryServices.DirectorySearcher object. The string following this type accelerator sets the object's SearchFilter property to find all user objects, and the FindAll method starts the search. The output is a list of System.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 and retrieve the object's memberof attribute. Using the $searcher variable from the previous example, we can use the FindOne method (instead of FindAll) to retrieve one result and output the user's group memberships:
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 a member.
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 memberof attribute. 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 to Use 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 script to 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 into alternate formats. You can use the NameTranslate object by creating the object and then calling its Init method to initialize it. For example, Listing 1 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 not have a type library, which .NET (and thus PowerShell) uses to provide easier access to COM objects. Fortunately, there is a workaround for this problem as 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.
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 Computers GUI.) 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 interpreted properly (see the Microsoft article "Distinguished Names" for more information). Fortunately, the Pathname COM object provides this capability. The script 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's groups. If you repeat this search for many user accounts, the repeated searching adds up to a lot of overhead. Retrieving the samaccountname attribute for each group in the memberof attribute that I mentioned in Problem #2 (Translating Between Naming Formats) also adds overhead. To meet this challenge, the script uses two global hash tables that cache results for improved performance.
Get-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] <String>] [-SearchScope <String>]
The -SearchLocation parameter is one or more DNs to search for user accounts. Because a DN contains commas (,), enclose each DN in single quotes (') or double quotes (") to prevent PowerShell from interpreting it as an array. The -SearchLocation parameter name is optional. The script also accepts pipeline 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 base object, 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 under it. 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, as discussed previously.
- Challenge #3 (Dealing with Special Characters): The script uses the get-escaped function, which uses the Pathname object to return DNs with the proper escape characters inserted where needed.
- Challenge #4 (Improving Performance): The script uses the $PrimaryGroups and $Groups hash tables. The $PrimaryGroups hash table's keys are primary group IDs, and its values are the primary groups' samaccountname attributes. The $Groups hash table's keys are the groups' DNs, and its values are 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 simplest application 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 in your 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
Set NameTranslate = CreateObject("NameTranslate")
NameTranslate.Init ADS_NAME_INITTYPE_GC, vbNull
Listing 2: Creating and Initializing the NameTranslate Object in PowerShell
$NameTranslate = new-object -comobject NameTranslate
[Void] $NameTranslate.GetType().InvokeMember("Init", "InvokeMethod",
$NULL, $NameTranslate, ($ADS_NAME_INITTYPE_GC, $NULL))