Find and Report Members of a Local Group

One of the things that a Sys Admin might want to know about is who exactly is in a group, such as Administrators. Getting it from one system is pretty simple, but pulling this data from multiple remote systems can be a little hectic, especially if trying to do this manually.

Luckily PowerShell can once again help us to get this information with little to no effort.

Excluding the help, I will break this function (Get-LocalGroupMember) up and explain what each part is doing to guide you along the way and then finish up with the function in action.

[cmdletbinding()]
    #region Parameters
    Param (
        [parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
        [Alias("Computer","__Server","IPAddress","CN","dnshostname")]
        [string[]]$Computername = $env:COMPUTERNAME,
        [parameter()]
        [string]$Group = 'Administrators',
        [parameter()]
        [string[]]$ValidMember,
        [parameter()]
        [Alias("MaxJobs")]
        [int]$Throttle = 10
    )
    #endregion Parameters
    Begin {
        #region Functions
        #Function to perform runspace job cleanup
        Function Get-RunspaceData {
            [cmdletbinding()]
            param(
                [switch]$Wait
            )
            Do {
                $more = $false         
                Foreach($runspace in $runspaces) {
                    If ($runspace.Runspace.isCompleted) {
                        $runspace.powershell.EndInvoke($runspace.Runspace)
                        $runspace.powershell.dispose()
                        $runspace.Runspace = $null
                        $runspace.powershell = $null                 
                    } ElseIf ($runspace.Runspace -ne $null) {
                        $more = $true
                    }
                }
                If ($more -AND $PSBoundParameters['Wait']) {
                    Start-Sleep -Milliseconds 100
                }   
                #Clean out unused runspace jobs
                $temphash = $runspaces.clone()
                $temphash | Where {
                    $_.runspace -eq $Null
                } | ForEach {
                    Write-Verbose ("Removing {0}" -f $_.computer)
                    $Runspaces.remove($_)
                }             
            } while ($more -AND $PSBoundParameters['Wait'])
        }
        #endregion Functions

 

Here we are setting up the parameters for the function which range from specifying a collection of computers to what local group you want to look at. A notable parameter is the ValidMember which allow you to specify a collection of members of a group that will result in the IsValid property being True, meaning that we expected the group to have these members. Great for running a baseline report of members of a specific group, such as Administrators.

Also we have the Get-RunspaceData function which will handle the background runspaces that get created to support the multithreading of the local group check.

  #region Splat Tables
        #Define hash table for Get-RunspaceData function
        $runspacehash = @{}

        $testConnectionHash = @{
            Count = 1
            Quiet = $True
        }

        #endregion Splat Tables

        #region ScriptBlock
        $scriptBlock = {
            Param ($Computer,$Group,$ValidMember,$testConnectionHash)
            Write-Verbose ("{0}: Testing if online" -f $Computer)
            $testConnectionHash.Computername = $Computer
            If (Test-Connection @testConnectionHash) {
		        $adsicomputer = [ADSI]("WinNT://$Computer,computer")
    	        $localgroup = $adsicomputer.children.find($Group)
                If ($localGroup) {
    	            $localgroup.psbase.invoke("members") | ForEach {
                        Try {
                            $member = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
                            If ($ValidMember-notcontains $member) {
                                New-Object PSObject -Property @{
                                    Computername = $Computer
                                    Group = $Group
                                    Account = $member
                                    IsValid = $FALSE
                                }
                            } Else {
                                New-Object PSObject -Property @{
                                    Computername = $Computer
                                    Group = $Group
                                    Account = $member
                                    IsValid = $TRUE
                                }
                            }
                        } Catch {
                            Write-Warning ("{0}: {1}" -f $Computer,$_.exception.message)
                        }
                    }
                } Else {
                    Write-Warning ("{0} does not exist on {1}!" -f $Group,$Computer)
                }  
            } Else {
                Write-Warning ("{0}: Unable to connect!" -f $Computer)
            }         
        }
        #endregion ScriptBlock

        #region Runspace Creation
        Write-Verbose ("Creating runspace pool and session states")
        $sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)
        $runspacepool.Open()  
        
        Write-Verbose ("Creating empty collection to hold runspace jobs")
        $Script:runspaces = New-Object System.Collections.ArrayList        
        #endregion Runspace Creation
    }

The next section sets up some collections that will house the runspace jobs and supply a splatting hash table for the Test-Connection cmdlet that is used later on to verify if a system is online or not.

The $scriptblock is just that, a scriptblock that we will pass to the runspace to run in the background to get the members of a local group. To get the group, I use the [adsi] type accelerator to connect to the local or remote system. From there I find the group that is specified on the $Group parameter and then pull all of the members of the group using GetType().InvokeMember(). More information about this interface can be found at http://msdn.microsoft.com/en-us/library/aa772237(VS.85).aspx.

Process {
        ForEach ($Computer in $Computername) {
            #Create the powershell instance and supply the scriptblock with the other parameters 
            $powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($Group).AddArgument($ValidMember).AddArgument($testConnectionHash)
           
            #Add the runspace into the powershell instance
            $powershell.RunspacePool = $runspacepool
           
            #Create a temporary collection for each runspace
            $temp = "" | Select-Object PowerShell,Runspace,Computer
            $Temp.Computer = $Computer
            $temp.PowerShell = $powershell
           
            #Save the handle output when calling BeginInvoke() that will be used later to end the runspace
            $temp.Runspace = $powershell.BeginInvoke()
            Write-Verbose ("Adding {0} collection" -f $temp.Computer)
            $runspaces.Add($temp) | Out-Null
           
            Write-Verbose ("Checking status of runspace jobs")
            Get-RunspaceData @runspacehash        
        }
    }
    End {
        Write-Verbose ("Finish processing the remaining runspace jobs: {0}" -f (@(($runspaces | Where {$_.Runspace -ne $Null}).Count)))
        $runspacehash.Wait = $true
        Get-RunspaceData @runspacehash
    
        #region Cleanup Runspace
        Write-Verbose ("Closing the runspace pool")
        $runspacepool.close()  
        $runspacepool.Dispose() 
        #endregion Cleanup Runspace
    } 
}

 

The final section covers adding each computer to the runspace pool if it has a good network connection from wherever it is at. Each runspace is added to the runspace jobs collection and the Get-RunspaceData function is called after each computer is added to make sure that no runspaces have finished and if they have finished, then it will properly dispose of the object.

At the very end we wait for the rest of the runspaces to finish up and dispose of the rest of the runspaces along with the runspace pool.

Now lets put this function into action! First we need to dot source the script to actually load the function into the current PowerShell session.

. .\Get-LocalGroupMember

If we do not do this, then the function will simply not work. In fact, running the script like you normally would will result in nothing happening.

Get-LocalGroupMember -ValidMember "Administrator"

image

As you can see, all members of the Administrators group are presented in a report and because I specified Administrator as a ValidMember so it can be filtered out if needed by only looking for False entries under the IsValid property.

Get-LocalGroupMember -ValidMember "Administrator" `
-Computername dc1.rivendell.com,boe-pc

 

image

Using this, you could set the function up as scheduled job to report on groups that do not meet a baseline of users or whatever else you can think of.

Download the Function

Script Repository

This entry was posted in powershell, scripts and tagged , . Bookmark the permalink.

5 Responses to Find and Report Members of a Local Group

  1. kostyanius says:

    Please advise how to install current script in order to run it with powershell like module command.
    Besides if to run it like the script it does not show anything. What is wrong?

  2. erikcurtis says:

    I am able to pass a list of computers via Get-ADComputer but that only does one computer at a time. Is there some way to use Get-ADComputer and have it process multiple at a time?

  3. Pingback: Get All Members of a Local Group Using PowerShell | Learn Powershell | Achieve More

  4. jabrasser says:

    Very nice script Boe, I like what you did with the runspaces, utilizing them in this way really adds to your script. When I saw the title I expected a one-liner utilizing ADSI, but you put in some good effort to make the script more versatile. Keep up the good work!

Leave a comment