3

PowerShell question for you savvy folks. Is it possible to filter a Get-ADGroup command based on group size (aka only return groups greater than x members)? I'm trying to filter in a more efficient way than: Get-ADGroup -Filter * and then running the member count check after, as in | where {$_.members.count -gt 10,000} or something.

But I'm having a hard time doing any kind of member count check on the initial filter so I don't have to return every group object in the individual domain and then check membership count. This is because the AD instance I'm querying against has a huge amount of AD groups that takes a long time to pull all groups first, then check.

I've tried variations of the below, but I'm guessing that the initial "members" property doesn't exist in the attribute set you can query against:

Get-ADGroup -Properties members -Filter {members -ge 10,000}

Any help is appreciated!

Johnny Welker
  • 104
  • 1
  • 7
  • 2
    The only way I see for improving this is filtering for only groups that have any members instead of all groups and using a `foreach` loop + `if` for filtering instead of `Where-Object`. – Santiago Squarzon Apr 12 '22 at 21:38
  • The other alternative would be multi threading, you should also clarify if you're searching for groups with more than 10k members where members are user objects only or all object class, and if you're looking for direct membership or recursive. – Santiago Squarzon Apr 12 '22 at 22:09
  • 1
    Note: [Don't use ScriptBlocks for the -Filter parameter](https://stackoverflow.com/questions/20075502/get-aduser-filter-will-not-accept-a-variable/44184818#44184818). I also cover some additional gotcha's with using ScriptBlocks and relying on the cmdlet to expand your variables for you from string literals in [my own answer here](https://stackoverflow.com/questions/51218670/how-to-effectively-use-the-filter-parameter-on-active-directory-cmdlets/51218671#51218671) in the **Things to Avoid** section. – codewario Apr 14 '22 at 16:27
  • 1
    @SantiagoSquarzon good point about at least being able to remove 0 membership groups from the query. In my actual script for this, I am using foreach, if, etc as I am cycling through several domains and logging results. I'm really only concerned with direct membership user objects currently. – Johnny Welker Apr 14 '22 at 19:44
  • Mathias's answer covers the filter for groups having at least 1 member, the only thing that would need to be changed is the use of a `foreach` loop + `if` for filtering instead of the slow `where-object`. or you could use `| & { process { if($_.members.count -gt 10,000) { $_ } }` which would also be very fast compared to `where-object` – Santiago Squarzon Apr 14 '22 at 19:46

2 Answers2

2

Is it possible to filter a Get-ADGroup command based on group size (aka only return groups greater than x members)?

No!

The LDAP query filter syntax supported by Active Directory does not have any facility for specifying the count of multi-valued attributes.

You need to query the directory for groups that have any members, then count the result set client-side:

Get-ADGroup -LDAPFilter '(&(objectClass=group)(member=*))' -Properties member |Where-Object {
    $_.member.Count -gt 10000
}
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • Like was also mentioned in the comments by Santiago, I like the addition of members=* here. At the scale I'm dealing with, even slight improvements like this help. Thanks for the info on the multi-valued as well. It is, unfortunately, what I expected. – Johnny Welker Apr 14 '22 at 19:56
1

This is how you can improve the speed of your query with multi-threading, using Runspaces in this example, the main idea is to get all the OUs in the Domain and let each runspace query a specific OU at the same time (as many queries as defined in $threads).

This should improve the speed of the script by a big amount, however, this requires tweaking, if you have too many threads running at the same time it is likely that it can fail.

$ErrorActionPreference = 'Stop'

# define the params that will be passed to the runspaces
$params = @{
    LDAPFilter  = "(member=*)"
    SearchScope = 'OneLevel'
    Properties  = 'member'
}

# define the logic of the runspace
$scriptblock = {
    param($params)

    foreach($group in Get-ADGroup @params) {
        if($group.Member.Count -gt 10000) {
            $group
        }
    }
}

try {
    # get all OUs
    $ous = (Get-ADOrganizationalUnit -Filter *).DistinguishedName
    # get all Domain Controllers available
    # we don't want to make too many queries to the same DC!!
    $dcs = (Get-ADDomainController -Filter *).Name
    # define the number of threads that can run at the same time
    # maybe you could use `$dcs.Count` as Threads
    # this depends on your server's resources and your network
    $threads = 10
    $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $threads)
    $RunspacePool.Open()
    
    $runspace = foreach($ou in $ous) {
        $params['SearchBase'] = $ou
        $params['Server'] = $dcs[$i++ % $dcs.Count]
        $ps = [powershell]::Create()
        $ps.AddScript($scriptblock).AddParameter('params', $params)
        $ps.RunspacePool = $RunspacePool

        [pscustomobject]@{
            Instance = $ps
            Task     = $ps.BeginInvoke()
        }
    }

    # capture the output from each runspace here!
    $result = foreach($r in $runspace) {
        $r.Instance.EndInvoke($r.Task)
        $r.Instance.foreach('Dispose')
    }
}
catch {
    Write-Warning $_.Exception.Message
}
finally {
    $runspace.foreach('Clear')
    $RunspacePool.foreach('Dispose')
}

$result | Format-Table
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37