Skip to main content

Count-MembersInADGroupNested

##########################################
# AUTHOR    : Ryan Mutschler
# DATE      : 4-2-2025
# EDIT      : 4-2-2025
# PURPOSE   : Count members of an Active Directory group, including nested groups.
# REPOSITORY: https://github.com/MutschlerHomeTech/Public-Scripts/blob/master/1.%20Full%20Scripts/Windows/File%20System/Create-WorkloadFolder.ps1
# WIKIPEDIA : https://wikipedia.mutschlerhome.com/books/windows-scripts/page/create-workloadfolder-v12
#
# VERSION   : 1.0     (Initial release)
##########################################

# Count-ADGroupMembers.ps1
# Script to count members of an Active Directory group, including nested groups

# Import the Active Directory module
Import-Module ActiveDirectory

function Get-ADNestedGroupMembers {
    param (
        [Parameter(Mandatory = $true)]
        [string]$GroupName
    )

    # Initialize a hashtable to track processed groups (prevents infinite loops with circular nesting)
    $processedGroups = @{}
    
    # Initialize a System.Collections.ArrayList to store all members (more efficient than arrays for adding items)
    $allMembers = New-Object System.Collections.ArrayList

    # Define a recursive function to get members including nested groups
    function Get-NestedMembers {
        param (
            [Parameter(Mandatory = $true)]
            [string]$GroupDistinguishedName
        )

        # Skip if we've already processed this group
        if ($processedGroups.ContainsKey($GroupDistinguishedName)) {
            return
        }

        # Mark this group as processed
        $processedGroups[$GroupDistinguishedName] = $true

        # Get direct members of the group
        $groupMembers = Get-ADGroupMember -Identity $GroupDistinguishedName -ErrorAction SilentlyContinue

        foreach ($member in $groupMembers) {
            # Add all members to our collection using ArrayList.Add() method
            [void]$allMembers.Add($member)

            # If the member is a group, process its members recursively
            if ($member.objectClass -eq 'group') {
                Get-NestedMembers -GroupDistinguishedName $member.distinguishedName
            }
        }
    }

    # Get the group's distinguished name
    try {
        $group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
        $groupDN = $group.DistinguishedName
    }
    catch {
        Write-Error "Error finding group '$GroupName': $_"
        return $null
    }

    # Start the recursive process
    Get-NestedMembers -GroupDistinguishedName $groupDN

    # Return all unique members (remove duplicates)
    return $allMembers | Sort-Object -Property objectGUID -Unique
}

# Function to display counts by type
function Show-MemberTypeCounts {
    param (
        [Parameter(Mandatory = $true)]
        [array]$Members
    )

    $userCount = ($Members | Where-Object { $_.objectClass -eq 'user' }).Count
    $groupCount = ($Members | Where-Object { $_.objectClass -eq 'group' }).Count
    $computerCount = ($Members | Where-Object { $_.objectClass -eq 'computer' }).Count
    $otherCount = ($Members | Where-Object { $_.objectClass -notin 'user','group','computer' }).Count
    
    Write-Host "`nMembers by type:"
    Write-Host "  Users: $userCount"
    Write-Host "  Groups: $groupCount"
    Write-Host "  Computers: $computerCount"
    Write-Host "  Other objects: $otherCount"
}

# Main script
$groupName = Read-Host "Enter the name of the Active Directory group"

Write-Host "`nRetrieving members of group '$groupName' (including nested groups)..."
$members = Get-ADNestedGroupMembers -GroupName $groupName

if ($null -ne $members) {
    $totalCount = $members.Count
    Write-Host "`nTotal unique members: $totalCount"
    
    # Show breakdown by object type
    Show-MemberTypeCounts -Members $members

    # Optionally export to CSV
    $exportCsv = Read-Host "`nExport members to CSV? (Y/N)"
    if ($exportCsv -eq 'Y' -or $exportCsv -eq 'y') {
        # Ensure C:\Temp directory exists
        if (-not (Test-Path -Path "C:\Temp" -PathType Container)) {
            try {
                New-Item -Path "C:\Temp" -ItemType Directory -Force | Out-Null
                Write-Host "Created directory: C:\Temp"
            } catch {
                Write-Error "Failed to create C:\Temp directory: $_"
                return
            }
        }
        
        $csvPath = "C:\Temp\$groupName-members.csv"
        $members | Select-Object Name, SamAccountName, objectClass, distinguishedName |
            Export-Csv -Path $csvPath -NoTypeInformation
        Write-Host "Members exported to: $csvPath"
    }
}