1

First, I cannot get into why I need this data and I cannot get into specifics about the network. You'll have to trust me there is no other way to get this data other than a PowerShell script to run LDAP queries.

I am working with a network that has multiple forests and multiple domains. There is a trust between all the forests. I am logged into one domain on one of the forests but because of the trust I can query all of them.

I have a CSV file with millions of AD groups. I need to find all the direct members of everyone of the millions of AD groups. A lot of memberships are cross-domain which means I cannot just use the member property of the AD group and have to, instead, query every domain and check for memberOf.

I have a PowerShell script that gets this data. For various reasons I cannot share my code but here is what it does:

  1. creates an array of System.DirectoryServices.DirectorySearcher objects for all of my domains
  2. iterate through the CSV file that has a list of every AD group and its DN
  3. for each DN, loop over the DirectorySearcher array and find all objects that are a memberOf the AD group in that DirectorySearcher ((memberOf=$adGroupDN))

The code works. But since I'm dealing with an input list with millions of AD groups the script is awfully slow. Based on my test run calculations it will take more than 2 weeks to get all of the data I need.

I'm wondering if there is a better/faster way to do this?

I thought maybe I could use threading or something but I am not sure if that will help nor am I sure where to start.

Any advise is greatly appreciated.

Adding some additional details...

  • My input list are millions of unique group DNs
  • I have multiple different forests/domains
  • My input group DNs are spread across all the forests/domains
  • The groups that are in my input list of group DNs span different forests/domains (domain1\group1 from my input list has domain2\group2 as a member)
  • I need to get a complete list of every group that is in the groups from my input list
  • Because of cross-domain memberships I cannot rely on the member attribute of the my input groups. The only way I know to get it is query every DC/domain for all groups that are are memberOf the groups from my input list.
  • I can only use PowerShell
  • I do not have the ActiveDirectory module and can only use the .NET DirectorySearcher
  • At a high level my code looks like this:

    $arrayOfDirectorySearcherObjectsForEachDCInMyNetwork = ... code to create an array of System.DirectoryServices.DirectorySearcher objects, one for each DC/domain in my network
    
    Foreach ($groupDN in $inputListOfUniqueGroupDNs)
    {
        Foreach ($domain in $arrayOfDirectorySearcherObjectsForEachDCInMyNetwork)
        {
            ...
    
  • The only way I can think of making it faster is to multi-thread the second for loop where it queries multiple DCs/domains at the same time using runspaces but I cannot figure out how to do this...
IMTheNachoMan
  • 5,343
  • 5
  • 40
  • 89
  • 1
    Such a huge organization is likely to have a support contract with Microsoft. Have you considered asking them for recommendations? – vonPryz Feb 09 '18 at 19:54
  • It's hard to help make your script faster when you assume where we can make changes. How are you reading the input files? How are you using the DirectorySearcher. I get that you can't include you code but could you include something more? Either way you could do some more debugging on your own to get this. Do you have remote access to those other forests? Perhaps it would be faster to execute there and pull findings? Have you considered running jobs for parallel execution? – Matt Feb 10 '18 at 05:23
  • I added some detail that I hope helps. I can add more code examples if needed. I would love direction on running jobs in parallel. I tried reading up on runspaces but can't figure it out. – IMTheNachoMan Feb 14 '18 at 02:38
  • Have you considered just retrieving all objects having a 'memberOf' attribute from all domain controllers and do all this lookup logic in local memory? – veefu Feb 14 '18 at 06:08
  • @veefu: If there are millions and millions of objects in the network, would that still be efficient? – IMTheNachoMan Feb 14 '18 at 11:06
  • @nacho I'm making the guess that redundant queries in your script are what is slowing things down. You already retrieved millions and millions of group DNs. How long did that take? – veefu Feb 14 '18 at 11:10
  • @veefu: Why do you feel there are redundant queries? My input list is a list of millions of unique group DNs. I need to get the members of each of those group DNs. Where would there be redundancy? And the input list of group DNs was provided to me so I am not sure how long it took. I suspect a few hours but that's just 1 call to the GC of each domain. – IMTheNachoMan Feb 14 '18 at 14:45
  • @nacho in search 1, you get all objects where memberof contains group1. This returns member1. In search 2 you get all objects where memberof contains group2. This returns member1 again because it is member of both. Member1 was transmitted twice. That is the potential redundancy which may be very costly if objects are members of many groups. – veefu Feb 14 '18 at 15:48

3 Answers3

2

Running the script on a domain controller would give you a slight advantage, if it's an option. But otherwise multi-threading is likely your best bet.

Look into using Start-Job. There's an example here.

That said, I question this:

A lot of memberships are cross-domain which means I cannot just use the member property of the AD group and have to, instead, query every domain and check for memberOf.

Group scope is important here. If all of your groups are Universal, then either way shouldn't make a difference (whether you look at member on the group or memberOf on the users).

But it's important to note that memberOf will not show Domain Local groups groups on a different domain (even in the same forest).

The member attribute on a group is always the authoritative source for the members. Yes, getting the details of an account on a trusted domain is a little tougher, but it can be done.

Here is a PowerShell function that will pull the "domain\username" of every member of a group, including those in nested groups.

function OutputMembers {
    param([string] $groupDn)

    foreach ($m in ([ADSI]("LDAP://" + $groupDn)).member) {
        $member = [ADSI]("LDAP://" + $m)
        $member.objectClass
        if ($member.objectClass -eq "group") {
            #this member is a group so pull the members of that group
            OutputMembers $member.distinguishedName
        } else {
            #"msDS-PrincipalName" is not loaded by default, so we have to tell it to get it
            $member.Invoke("GetInfoEx", @("msDS-PrincipalName"), 0)
            if ([string]::IsNullOrEmpty($member."msDS-PrincipalName")) {
                #member is on a trusted domain, so we have to go look it up
                $sid = New-Object System.Security.Principal.SecurityIdentifier ($member.objectSid[0], 0)
                $sid.Translate([System.Security.Principal.NTAccount]).value
            } else {
                $member."msDS-PrincipalName"
            }
        }
    }
}

With that, you call that function with the distinguishedName of each group, like:

OutputMembers "CN=MyGroup,OU=Groups,DC=domain,DC=com"
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • The `member` attribute won't show cross domain memberships. From what I know, if `domain1\group1` has `domain2\group2` as a member then the `member` attribute of `domain1\group1` **will not** show `domain2\group2` and the only way to check it is query `domain2` for all groups that are `memberOf` of `domain1\group1`. Am I wrong? – IMTheNachoMan Feb 14 '18 at 02:24
  • Also I cannot use `Import-Module ActiveDirectory` which is why I am using the .NET `DirectorySearcher`. – IMTheNachoMan Feb 14 '18 at 02:25
  • I'm adding more details to the original question. – IMTheNachoMan Feb 14 '18 at 02:28
  • You are wrong about that. The `member` attribute is the ultimate authority about what is a member of the group. If it's not in the `member` attribute, it's not in the group. The opposite is not true: if a group is not in the `memberOf`, that doesn't necessarily mean the account is not a member of a group. – Gabriel Luci Feb 14 '18 at 13:12
  • I updated my answer so the code doesn't use the `ActiveDirectory` module. Just ADSI. It'll also recursively search nested groups. You'll just have to figure out how to pull the DN's from the file and use `Start-Job` to run things in parallel. – Gabriel Luci Feb 14 '18 at 13:55
  • Is `Start-Job` the best approach? I thought `runspaces` is preferred? – IMTheNachoMan Feb 14 '18 at 14:46
  • And will `([ADSI]("LDAP://" + $groupDn)).member` list **all** members, including cross domain members? – IMTheNachoMan Feb 14 '18 at 14:47
  • Yes, it will. If the member is on a trusted domain, it shows up as a foreign security principal, which contains the SID of the actual account on the external domain, which is why there's that piece of code that translates the SID to the domain\username. – Gabriel Luci Feb 14 '18 at 14:53
  • Oh I see. This is brilliant. Thank you! – IMTheNachoMan Feb 14 '18 at 15:38
  • @Gabriel Luci: SecurityIdentifier.Translate method is terribly slow,especially when it comes to trusted domains. Also the method sometimes is unable to resolve sid for unknown reasons, when I can do it manually. I assume, that under the hood it relies on LookupAccountSid and I believe, that there's something wrong with parameters passed to that method. For FSP I suggest to extract sid from DN and manually bind to GC of the requested domain (cache that contains pairs domain SID - GC can be created, so by FSP domain sid part a bind can be done to a certain GC) using sid binding, which is fast. – oldovets Feb 14 '18 at 23:55
  • ...For other DN's, which are all in the same forest, direct DN bind can be done to any GC. Then in both cases a call to DirectoryEntry.RefreshCache with msDs-PrincipalName attribute gives us NT4 name and solves the problem (it will return administrators not as contoso\administrators but as builtin\administrators but there is a workaround to query and cache netbios name and create NT4 name manually). What do you think? – oldovets Feb 15 '18 at 00:01
  • Finally, I recommend to additionally process situations where cyclic membership may occur. A call to current version of OutputMembers in that case will result in stack overflow. The method needs a cache with already processed groups – oldovets Feb 15 '18 at 00:15
  • Those are all good suggestions. I expected that the code I provided would need a lot of tweaking. – Gabriel Luci Feb 15 '18 at 14:00
1

I presume that the performance issue in step 3 as that presumably has an embedded loop (that might even be recursive if you look for indirect group memberships as well):

Foreach ($UserDN in $UserDNs) {
    ...
    Foreach ($GroupDN in $GroupDNs) {
        ...

Everything in the inner loop is very important for the performance of your script as that will be invoked $UserDNs.Count * $GroupDNs.Count times!
I suspect that there are a lot of redundant LDAP queries in the inner loop (for user that are in the same group) and therefore focus on that and build a kind of custom caching of every redundant query to the server to overcome this. Something like:

$MemberCache = @{}

Function GetMembers([String]$GroupDN) {
    If (!$MemberCache.ContainsKey($GroupDN)) {
        $MemberCache[$GroupDN] = @{}    #HashTables are much faster then using the contains method on an array
        # retrieve all members of the AD group in that DirectorySearcher
        ForEach ($Member in $Members) {$MemberCache[$GroupDN].$Member = $True}
    }
    $MemberCache[$GroupDN]
}

Function IsMember([String]$DN, [String]$GroupDN) {
    (GetMembers($GroupDN)).ContainsKey($DN)
}

The general idea is that you should not remotely redo the "find all objects that are a memberOf the AD group in that DirectorySearcher ((memberOf=$adGroupDN))" for the same $adGroupDN (any group you already queried before) but retrieve the required information from a local hash table (cache).

iRon
  • 20,463
  • 10
  • 53
  • 79
0

There are several optimizations can be done here. These optimizations are not related to Powershell, but to the algorithm itself.

  1. Powershell is not designed to perform such kind of tasks. C\C++ or at least C# should be used instead.
  2. Create and maintain connection to RootDse object of each global catalog that you're querying information from until all job is done. In this case all AD queries will use one single cached connection to AD, which significantly increases performance.
  3. Thumbs up to iRon. Create a cache for all queried groups. For example, if you queried membership for group A and group A is a member of group B, there is no need to query memberhip of group A again. Of course, in your case you just cannot simply store all membership in memory, so you need to create some kind of storage where membership will be saved
  4. Read groups from CSV in parallel. Make several threads. To combine it with group cache you need to operate 2 caches. One for already queried groups and the other one for pending groups, that threads are querying at the moment (to avoid double query the same group in different threads). These caches should be thread safe of course. If one thread sees, that another is querying the group, this thread can skip current group and query any other, and return back later.
  5. For foreign security principals (users and groups from trusted domains) use SID binding. You can extract sid for FSP from Member DN. Direct binding works much faster than DirectorySearcher

Be aware of:

  1. Thumbs up to Gabriel Luci. You need to query member, not memberOf attribute,
  2. Don't forget about nested groups (group A -> group B -> user U). User is a member of group B as well a group A. This rule applies to trusted domain groups as well
  3. Groups may be members of each other. E. g. A -> B -> C -> A or even A -> B -> A. You have to handle this in your script
oldovets
  • 695
  • 4
  • 9
  • Not enough room to reply in depth so I'll keep it short. 1) Can only use PS. 2) Already doing. 3) Already doing. All the groups I am checking are already unique. I've got millions of unique A groups. 4) Would love more details on this because I'm not sure how to do it. 5) Not sure what this mean but I'll research. – IMTheNachoMan Feb 14 '18 at 02:22
  • I'm adding more details to the original question. – IMTheNachoMan Feb 14 '18 at 02:28