Public/activedirectory/Get-ADGroupMembership.ps1

#Requires -Version 5.1

function Get-ADGroupMembership {
    <#
    .SYNOPSIS
        Retrieves members of an Active Directory group
 
    .DESCRIPTION
        Queries Active Directory for group membership details with optional recursive
        enumeration. When the Recursive switch is used, both direct and nested members
        are returned with an IsDirect flag indicating membership type.
 
    .PARAMETER Identity
        One or more group identities to query. Accepts SamAccountName or DistinguishedName.
 
    .PARAMETER Recursive
        When specified, retrieves all nested members transitively and marks each as direct or nested.
 
    .PARAMETER Server
        Specifies the Active Directory Domain Services instance to connect to.
 
    .PARAMETER Credential
        Specifies the credentials to use for the Active Directory query.
 
    .EXAMPLE
        Get-ADGroupMembership -Identity 'Domain Admins'
 
        Retrieves direct members of the Domain Admins group.
 
    .EXAMPLE
        Get-ADGroupMembership -Identity 'Domain Admins' -Recursive -Server 'dc01.contoso.com'
 
        Retrieves all nested members from a specific domain controller with direct/nested flags.
 
    .EXAMPLE
        'Domain Admins', 'Enterprise Admins' | Get-ADGroupMembership
 
        Retrieves direct members of multiple groups via pipeline input.
 
    .OUTPUTS
        PSWinOps.ADGroupMember
        Returns custom objects with group name, member details, object class,
        and IsDirect flag sorted by ObjectClass then MemberName.
 
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-04-03
        Requires: PowerShell 5.1+ / Windows only
        Requires: ActiveDirectory module
 
    .LINK
        https://github.com/k9fr4n/PSWinOps
 
    .LINK
        https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-adgroupmember
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Identity,

        [Parameter()]
        [switch]$Recursive,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Server,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]$Credential
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        try {
            Import-Module -Name 'ActiveDirectory' -ErrorAction Stop
        }
        catch {
            throw "[$($MyInvocation.MyCommand)] Failed to import ActiveDirectory module: $_"
        }

        $adParams = @{}
        if ($PSBoundParameters.ContainsKey('Server')) {
            $adParams['Server'] = $Server
        }
        if ($PSBoundParameters.ContainsKey('Credential')) {
            $adParams['Credential'] = $Credential
        }
    }

    process {
        foreach ($identityValue in $Identity) {
            try {
                Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying group: $identityValue"

                $group = Get-ADGroup -Identity $identityValue @adParams -ErrorAction Stop
                $resolvedGroupName = $group.Name

                $memberResults = [System.Collections.Generic.List[object]]::new()

                if ($Recursive) {
                    Write-Verbose -Message "[$($MyInvocation.MyCommand)] Retrieving recursive membership for: $resolvedGroupName"

                    $directMembers = Get-ADGroupMember -Identity $identityValue @adParams -ErrorAction Stop
                    $allMembers = Get-ADGroupMember -Identity $identityValue -Recursive @adParams -ErrorAction Stop

                    $directDNSet = [System.Collections.Generic.HashSet[string]]::new(
                        [System.StringComparer]::OrdinalIgnoreCase
                    )
                    foreach ($directMember in $directMembers) {
                        [void]$directDNSet.Add($directMember.DistinguishedName)
                    }

                    foreach ($member in $allMembers) {
                        $isDirect = $directDNSet.Contains($member.DistinguishedName)
                        $memberResults.Add([PSCustomObject]@{
                            PSTypeName        = 'PSWinOps.ADGroupMember'
                            GroupName         = $resolvedGroupName
                            MemberName        = $member.Name
                            SamAccountName    = $member.SamAccountName
                            ObjectClass       = $member.ObjectClass
                            DistinguishedName = $member.DistinguishedName
                            IsDirect          = $isDirect
                            Timestamp         = Get-Date -Format 'o'
                        })
                    }
                }
                else {
                    $directMembers = Get-ADGroupMember -Identity $identityValue @adParams -ErrorAction Stop

                    foreach ($member in $directMembers) {
                        $memberResults.Add([PSCustomObject]@{
                            PSTypeName        = 'PSWinOps.ADGroupMember'
                            GroupName         = $resolvedGroupName
                            MemberName        = $member.Name
                            SamAccountName    = $member.SamAccountName
                            ObjectClass       = $member.ObjectClass
                            DistinguishedName = $member.DistinguishedName
                            IsDirect          = $true
                            Timestamp         = Get-Date -Format 'o'
                        })
                    }
                }

                $memberResults | Sort-Object -Property 'ObjectClass', 'MemberName'
            }
            catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to query group '$identityValue': $_"
                continue
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}