Avatar of Indie101
Indie101
 asked on

Powershell script to list all groups, nested groups and their members in a csv file

Hi,

I know I can use

Get-AdGroupMember "Name of group"

-Recursive| Select DistinguishedName

To find the nested groups, I want to use this with get-adgroup -filter *  | sort name | select Name (realise this is pretty basic)

Want to pipe it to | export-csv C:\test.csv -NotypeInformation

Do I have to have the name of each group which is nested? Is there a handy way to do this? Thanks
PowershellActive Directory

Avatar of undefined
Last Comment
Indie101

8/22/2022 - Mon
Chris Dent

Is this what you had in mind?
Get-ADGroup -filter * | Select-Object Name, @{n='Members';e={ $_ | Get-ADGroupMember -Recursive | Select-Object Name }}

Open in new window

Indie101

ASKER
Thanks Chris looks great would you mind giving a brief explanation  of @{n='Members';e={ $_ | Get-ADGroupMember -Recursive | Select-Object Name }} just for future reference

It returns all the group names, but only members when that group has one member, is there a way to show all members?

I know the @ turns list contents into an array just investigating further :)
Chris Dent

Certainly.

Select-Object is normally used to pick specific properties from an object (or collection / array of objects).
Get-Process | Select-Object Name, Id

Open in new window

Select-Object can also be used to create custom properties. This feature is useful when there is a need to rename a property, or add related information.

Custom properties are created using a hashtable (@{}), an associative array (keys and values). The hashtable must contain two keys, Name or Label, and Expression. For example:
@{Name = 'NewProperty'; Expression = { 'Code to generate the value' }}
@{Label = 'NewProperty'; Expression = { 'Code to generate the value' }}

Open in new window

The label key, while permissible, is rarely used; name is the most common choice. Select-Object allows short-hand, the keys Name and Expression can be shortened to "n" and "e" respectively.
@{n = 'NewProperty'; e = { 'Code to generate the value' }}

Open in new window

The example below selects 3 properties from Get-Process. The third value, WorkingSet, which holds the amount of memory a process uses, is converted from bytes to MB using a custom property.
Get-Process | Select Name, Id, @{n='WorkingSet';e={ $_.WorkingSet / 1MB }}

Open in new window

Select-Object can include any number of custom properties, each separated by a comma. The example below adds a second property, the owner of the file (based on the NTFS Access Control List) if the Path property of the process is set.
Get-Process | Select Name,
    Id,
    @{n='WorkingSet';e={ [Math]::Round($_.WorkingSet / 1MB, 2) }},
    @{n='Owner';e={ if ($_.Path) { (Get-Acl $_.Path).Owner } }}

Open in new window

Experts Exchange has (a) saved my job multiple times, (b) saved me hours, days, and even weeks of work, and often (c) makes me look like a superhero! This place is MAGIC!
Walt Forbes
Indie101

ASKER
Thanks Chris it shows members when only one member in a group , is there a way to get all members for each group? thanks again, great explanation
ASKER CERTIFIED SOLUTION
Chris Dent

THIS SOLUTION ONLY AVAILABLE TO MEMBERS.
View this solution by signing up for a free trial.
Members can start a 7-Day free trial and enjoy unlimited access to the platform.
See Pricing Options
Start Free Trial
GET A PERSONALIZED SOLUTION
Ask your own question & get feedback from real experts
Find out why thousands trust the EE community with their toughest problems.
Indie101

ASKER
Got below error it had run two thirds through before giving it

Get-ADGroup : The server has returned the following error: invalid enumeration context.
At line:1 char:1
+ Get-ADGroup -filter * | Select-Object Name, @{n='Members';e={ $_ | Ge ...
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-ADGroup], ADException
    + FullyQualifiedErrorId : ActiveDirectoryServer:0,Microsoft.ActiveDirectory.Management.Commands.GetADGroup
Chris Dent

Hah it didn't like that much did it...

It always makes me sigh when the AD module does that. I think it's caused by a rate limiting mechanism. Somewhat reasonable because the recursive query is very intensive.

We can try and change the manner in which it gets that. Is it members which are groups you're interested in?
Get-ADGroup -filter * | Select-Object Name, @{n='Members';e={ Get-ADGroup -LdapFilter "(memberOf:1.2.840.113556.1.4.1941:=$($_.DistinguishedName))" | Select-Object -ExpandProperty Name }}

Open in new window

This uses a specific OID to expand membership.

Documentation for this one is here:

https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
Indie101

ASKER
Thanks it returns 258 groups, not the full amount as original did, 3048 groups originally you've done a lot more than I expected

I'm just not sure how to get all 3048 groups
Chris Dent

In the member list? Or is that the number of groups it's returning (from Get-ADGroup)?
Indie101

ASKER
3048 groups were returned from initial code you supplied

Get-ADGroup -filter * | Select-Object Name, @{n='Members';e={ $_ | Get-ADGroupMember -Recursive | Select-Object Name }}
This is the best money I have ever spent. I cannot not tell you how many times these folks have saved my bacon. I learn so much from the contributors.
rwheeler23
Chris Dent

The only change I've made is to the "Members" column. That's limited to groups at the moment, it can be opened up again:
Get-ADGroup -filter * | Select-Object Name, @{n='Members';e={ Get-ADObject -LdapFilter "(memberOf:1.2.840.113556.1.4.1941:=$($_.DistinguishedName))" | Select-Object -ExpandProperty Name }}

Open in new window

If you actually have groups with that number of members you need to go back to the original requirement, think about why you're doing this, and evaluate if this is really the best way. It's a horribly expensive operation, recursive group expansion is a big thing to ask a DC to do, doing it fora  large number of groups is not sensible at all.
Indie101

ASKER
Apologies Chris generally the groups have 20-30 members or less from what I have seen, 3048 is the number of groups present which is great

Just the original shows one member (when one member is present)
Chris Dent

I can't guarantee if this will work very well. It completely avoids the MS commands. Whether or not it works as is depends on your PowerShell version as well. I run PowerShell 5.1, syntax used is generally compatible down to PowerShell 4.0. Anything less than that is a bit less sure.

$PSVersionTable will show the version you're running.
function Get-ADSIGroup {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [String]$Name
    )

    $searcher = [ADSISearcher]"(&(objectClass=group)(objectCategory=group)(name=$Name))"
    $searcher.PageSize = 1000

    $searcher.PropertiesToLoad.AddRange(@('name', 'distinguishedName'))

    $searcher.FindAll() | ForEach-Object {
        [PSCustomObject]@{
            Name = $_.Properties['name'][0]
            DistinguishedName = $_.Properties['distinguishedName'][0]
        }
    }
}

function Get-ADSIGroupMember {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [String]$DistinguishedName,

        [Switch]$Recursive
    )

    process {
        if ($Recursive) {
            $LdapFilter = "(memberOf:1.2.840.113556.1.4.1941:=$DistinguishedName)"
        } else {
            $LdapFilter = "(memberOf=$DistinguishedName)"
        }
        
        $searcher = [ADSISearcher]$LdapFilter
        $searcher.PageSize = 1000

        $searcher.PropertiesToLoad.AddRange(@('name', 'distinguishedName'))

        $searcher.FindAll() | ForEach-Object {
            [PSCustomObject]@{
                Name = $_.Properties['name'][0]
                DistinguishedName = $_.Properties['distinguishedName'][0]
            }
        }
    }
}

Get-ADSIGroup -Name * | Select-Object Name, @{n='Members';e={ $_ | Get-ADSIGroupMember -Recursive | Select-Object -ExpandProperty Name }}

Open in new window

⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
Indie101

ASKER
Thanks Chris I'll check this out I'll close the question and award the marks, you really have done a lot more than the question asked for so thanks again (using PS 5.0)