Link to home
Create AccountLog in
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
Avatar of Chris Dent
Chris Dent
Flag of United Kingdom of Great Britain and Northern Ireland image

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

Avatar of Indie101
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 :)
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

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
Avatar of Chris Dent
Chris Dent
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
Create an account to see this answer
Signing up is free. No credit card required.
Create Account
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
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
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
In the member list? Or is that the number of groups it's returning (from Get-ADGroup)?
3048 groups were returned from initial code you supplied

Get-ADGroup -filter * | Select-Object Name, @{n='Members';e={ $_ | Get-ADGroupMember -Recursive | Select-Object Name }}
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.
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)
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

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)