In the Windows NT 4.0 world, group memberships were fairly simple to enumerate because only users or global groups could be members of local groups. Thus, the deepest possible nesting of permissions was having users in a global group and having that global group in a local group. This shallow nesting made it pretty simple to see which users had permissions to a particular resource.
Enter Windows 2000. In addition to letting you nest a global group in a local group, Win2K lets you nest local groups in other local groups. This ability makes identifying which users have permission to a resource much tougher. Although the Microsoft Management Console (MMC) Active Directory Users and Computers snap-in lets you drill down through the groups and visually see which users have permission, you can't see all this information in one display or output the results to a report.
Traditionally, the best command-shell utilities to use for enumerating group memberships have been the Local and Global utilities. Although these utilities do a great job of listing the members of local and global groups, respectively, they don't list the members of any groups that are nested in a local or global group. For quite some time, I had been struggling to write a script that enumerated the members of both first-level and nested groups. After some experimenting, I finally discovered that, by focusing on failures, I could use the Local and Global utilities to drill down through a group and enumerate the members of first-level and nested groups. After further experimenting, I was able to output this information to a comma-separated value (CSV) or tab-separated value (TSV) file for easy viewing in Microsoft Excel.
Script Counts on Command's Failures
By default, scriptwriters typically try to successfully execute commands and successfully capture those commands' output. Failure output is usually less important unless it affects a script's operation. However, in the EnumGroups.bat script, I depend on the Local and Global commands' failures to enumerate the members of first-level and nested groups.
Let's look at some sample failure output from the Local and Global commands. If you run the Local command against a known user account with a command such as
Local Fred domain1
you'll receive the failure message GetUsers - Unknown Error: 1376. If you run the Local command against a known global group with a command such as
Local AcctGG domain1
you'll receive the failure message GetUsers - Unknown Error: 1376. Notice that the failure messages are the same for both wrong inputs.
Now, let's run the Global command against a known user account with the command
Global Fred domain1
In this case, the failure message is 'Fred' group not found. If you run the Global command against a known local group with a command such as
Global SalesLG domain1
the failure message is 'SalesLG' group not found. Notice again that the failure messages are similar.
As an administrator, you can look at a named object and tell whether it's a user account or a group account because of your familiarity with your user and group naming conventions. However, in command-shell scripting, you don't have an easy way to determine whether an object is a user account, local group account, or global group account if your naming convention is domainname\username and domainname\groupname. So, you need to do some creative scripting.
You can determine what the named object is by initially assuming that every object you test is a local group. If you run the Local command against the item and that command succeeds, your assumption is correct and you have a local group. If the Local command fails and displays the error message GetUsers - Unknown Error: 1376, you know that you've eliminated the local group possibility, which means the object is either a global group or a user. At this point, you can run the Global command. If the Global command succeeds, you have a global group. If the command fails and displays an error message such as 'Fred' group not found, you know that the object is a user. Through this simple process of elimination, you can determine whether an object is a local group, global group, or user.
EnumGroups.bat uses this process of elimination to drill down through nested groups. For example, suppose the script uses the process of elimination and discovers that the first-level object is a local group. The script then uses the process of elimination to determine whether each object in that local group is a user or a nested group. If the script finds a nested group account at the second level, the script again uses the process of elimination to determine whether each object in the second-level group is a user or group. This process continues until either the specified maximum number of levels is reached or until the script runs out of objects to check. At this point, the script's flow returns to the point where it started the enumeration, and the script proceeds with the next object.
EnumGroups.bat is currently configured to retrieve groups and users down through eight levels. This number of levels will likely be sufficient for most environments. If you're unsure whether you need additional levels of enumeration, you can run the script as is. It will notify you when you've reached the eighth level and warn you that some groups might be listed but not enumerated.
Adding another level to the script is easy. You just copy the next-to-last module (level 7) and paste it in the location that the script specifies. I've added comments to the script to show exactly what code to copy and where to paste it. In the code you copy, you need to increment each numbered variable by one. For example, the %Group7% and %Dom7% variables need to become %Group8% and %Dom8%, respectively.
At the top of the script, you need to add several lines of code to the :CSVcode and :TSVcode sections. These sections specify the separators that the script uses to line up entries in the spreadsheet. For example, in the :CSVcode section, which Listing 1 shows, you'd add the line
after the line that callout A in Listing 1 highlights. In the header line, you'd add ,Level 9 immediately after Level 8.
How to Launch the Script
You must launch EnumGroups.bat in a command-shell window. The script will ask you to specify three items. When you enter these items, don't enclose them in quotation marks, even if the item contains spaces. The three items are:
- A group name. If you specify a universal group or built-in local group (e.g., Users group), the script will probably fail. The script doesn't work for universal groups because the Local and Global utilities aren't designed to list the members of such groups. The script doesn't work well for built-in local groups because the existence of the local machine user account can distort the results. Note that the script is sensitive to ampersands (&) and other reserved characters, which can cause the script to fail. However, group names seldom include reserved characters.
- A domain name. The script lets you specify a domain name or a local server name (\\servername).
- The output file's extension. By default, the script generates an output file named domainname-groupname, where domainname is the name of the domain you entered and groupname is the name of the group you entered. You must specify the file type by entering CSV if you want a CSV file, TSV if you want a TSV file, or TXT if you want a tab-delimited text file.
You can provide input to scripts in many ways. Commonly used approaches include using script arguments that you specify at runtime, manually editing variables in the script on each run, and using an input file. The lesser-known approaches include using the Choice command, which works well when users need to choose an option from a small static set of options, and using the Set /p command, which lets users enter any information they want.
For EnumGroups.bat, I used the Choice command to handle the input for the file types. The Choice command was ideal because this set of options consists of only three static items. The Choice command accepts only those options you specify; any other option will cause the script to stop. The script won't continue until the user enters a valid choice.
Although the Choice command is a good fit for capturing the file type, it wouldn't work for capturing the group name or domain name because many possible names exist and they can change frequently. The Set /p command works well for these two inputs. However, the Set /p command doesn't test whether the user's input is valid, so EnumGroups.bat contains some error-handling code to intercept bad input.
Sometimes you might need to change users' input to all uppercase or lowercase characters. I uppercased the domain group names that the Set /p command captures, so if a user enters sales for the domain name, the script changes that string to SALES. Having all uppercase letters isn't necessary to have EnumGroups.bat work properly, nor is it a formatting requirement for the script's output report. I just wanted to use this capability to demonstrate how you can change the case of strings because, although this procedure is obscure, it can be quite useful.
Listing 2 shows the code that uppercases the domain names. EnumGroups.bat uses the Setlocal Enabledelayedexpansion command, so this code expands the Dom variable while performing the character replacement. Typically, the character replacement syntax is
where oldchar is the character you want to replace and newchar is the replacement character. If you want to use variables to represent oldchar and newchar, the command would look like
However, this command would fail at runtime because of the nested percent signs. An alternative is to use exclamation points instead of percent signs as variable symbols so that the command would look like
This command lets you accomplish the character replacement without any errors.
Customize the Script
I tested EnumGroups.bat on machines running Win2K Professional Service Pack 3 (SP3) and Windows XP and was able to query group members in both Win2K and NT 4.0 domains. To get the script working in your environment, follow these steps:
- Download EnumGroups.bat from the Code Library on the Windows Scripting Solutions Web site (http://www.winscriptingsolutions.com).
- Put EnumGroups.bat in a separate folder, then make a backup copy of the original script. This backup will prove useful if you make a mistake when entering your path locations or when trying to increase the number of enumeration levels.
- By default, the script generates the output file to the same folder in which the script resides. If this folder isn't your preference, you can modify the code to use a different folder. The comments in the script detail the changes you need to make and even provide an option for you if you want to create the file in the %Temp% folder.
- Set the path leading to the folder that contains the Local, Global, and Choice utilities. In the line
- EnumGroups.bat opens the completed report file in Excel. If you don't have that application installed or you want to use another program to display the completed file, you need to change that section of code. The comments in the script explain how to adapt it.
change \\server\reskitShare to your folder's path. Be aware that when you embed the Local and Global commands inside a For command, they're sensitive to spaces in the path.
Now Give It a Try
After you have customized EnumGroups.bat, give the script an initial test. Enter the group name of a moderately complicated group that has several levels of nesting. If the script passes that test, try running it against your toughest group structures. Keep in mind that if you have a complicated structure, the script will take a while to run.