4 Challenges of Auditing AD Users and Groups

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:

[ 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:

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.

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.

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 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
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] ] [-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 (') 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 #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