Avatar of Bill Purvis
Bill Purvis
 asked on

Local Admins with Nested Groups

I would like a script that will find all Local Admins along with the Nested groups that may be apart.  

PowershellVB ScriptWindows Server 2019VBAScripting Languages

Avatar of undefined
Last Comment
Bill Purvis

8/22/2022 - Mon
Albert Widjaja

Bill Purvis

ASKER
Hello "Senior IT System Engineer",

I got an error, like the other guy did on that webpage:

Processing computer
Failed to compare two elements in the array.
    + CategoryInfo          : NotSpecified: (:) [Get-LocalGroupMember], InvalidOperationException
    + FullyQualifiedErrorId : An unspecified error occurred.,Microsoft.PowerShell.Commands.GetLocalGroupMemberCommand
    + PSComputerName        :computer

Open in new window


Can you help?
Thanks
Bill Purvis

ASKER
Again, I need a script that will find all Local Admins along with the Nested groups that may be apart.  
I started with Experts Exchange in 2004 and it's been a mainstay of my professional computing life since. It helped me launch a career as a programmer / Oracle data analyst
William Peck
oBdA

This will return all members of a local group.
Accepts pipeline input, so you can do something like
Get-Content .\ComputerList.txt | .\Get-LocalGroupMember.ps1 -Verbose | Export-Csv -NoTypeInformation -Path .\members.csv

Open in new window

[CmdletBinding()]
Param (
	[Parameter()]
	[string]$Group = 'Administrators',
	[Parameter(ValueFromPipeline=$true)]
	[string[]]$ComputerName = $ENV:ComputerName
)
Process {
	$ComputerName | ForEach-Object {
		$computer = $_
		Write-Verbose "Processing $($computer) ..."
		Try {
			If ([ADSI]::Exists("WinNT://$($computer)/$($Group),group")) {
				([ADSI]"WinNT://$($computer)/$($Group),group").Members() | ForEach-Object {
					If ($PSVersionTable.PSVersion.Major -ge 5) {
						$pathElements = $_.GetType.Invoke().InvokeMember('AdsPath', 'GetProperty', $null, $_, $null).Split('/', [StringSplitOptions]::RemoveEmptyEntries)
					} Else {
						$pathElements = ($_.GetType().InvokeMember('AdsPath', 'GetProperty', $null, $_, $null)).Split('/', [StringSplitOptions]::RemoveEmptyEntries)
					}
					[PSCustomObject]([ordered]@{
						ComputerName = $computer
						GroupName = $Group
						Member = If ($pathElements[-2] -eq 'WinNT:') {$pathElements[-1]} Else {"$($pathElements[-2])\$($pathElements[-1])"}
					})
				} | Sort-Object -Property Member
			} Else {
				Throw "Group '$($Group)' not found on $($computer)!"
			}
		} Catch {
			$PSCmdlet.WriteError($_)
		}
	}
}

Open in new window


Note that in the other question, there was a completely different error, about remote execution; yours is a bug in the cmdlet. Seems like Microsoft never tested Get-LocalGroupMember with a group that still contains a principal that was deleted, so the SID can't be resolved anymore:
Get-LocalGroupMember - Failed to compare two elements in the array. #2996
https://github.com/PowerShell/PowerShell/issues/2996
Bill Purvis

ASKER
Thanks oBdA... that only returns the Admins, Users and Groups.  It DOES NOT return the "Nested" ones that are in the Groups.  

Can you get those?
RobSampson

Hi Bill, given that local groups can have AD groups in them, I've checked the domain of the group, then nested the Get-Members call.  Be careful though, this can cause an endless loop if you have nested groups that are circular.

[CmdletBinding()]
Param (
   [Parameter()]
   [string]$Group = 'Administrators',
   [Parameter(ValueFromPipeline=$true)]
   [string[]]$ComputerName = $ENV:ComputerName
)
Process {

    Function Get-Members($GroupName) {
       Try {
          If ($GroupName.IndexOf("\") -ge 0) {
                $computer = $GroupName.Split("\")[0]
                $GroupName = $GroupName.Split("\")[1]
            }
            $GroupFound = Try{[ADSI]::Exists("WinNT://$($computer)/$($GroupName),group")} catch {$False}
            If ($GroupFound) {
                If ($computer -eq $ComputerName) {
                 ([ADSI]"WinNT://$($computer)/$($Group),group").Members() | ForEach-Object {
                    If ($PSVersionTable.PSVersion.Major -ge 5) {
                       $pathElements = $_.GetType.Invoke().InvokeMember('AdsPath', 'GetProperty', $null, $_, $null).Split('/', [StringSplitOptions]::RemoveEmptyEntries)
                    } Else {
                            $pathElements = ($_.GetType().InvokeMember('AdsPath', 'GetProperty', $null, $_, $null)).Split('/', [StringSplitOptions]::RemoveEmptyEntries)
                    }
                        $Member = If ($pathElements[-2] -eq 'WinNT:') {$pathElements[-1]} Else {"$($pathElements[-2])\$($pathElements[-1])"}
                        Get-Members $Member
                        Write-Verbose $Member
                    [PSCustomObject]([ordered]@{
                       ComputerName = $computer
                       GroupName = $GroupName
                       Member = $Member
                    })
                 } | Sort-Object -Property Member
                } Else {
                 Get-ADGroupMember $GroupName | ForEach-Object {
                    $Member = $_.samAccountName
                        Get-Members $Member                    
                    [PSCustomObject]([ordered]@{
                       ComputerName = $computer
                       GroupName = $GroupName
                       Member = $Member
                    })
                 } | Sort-Object -Property Member
                }
          } Else {
             #Throw "Group '$($GroupName)' not found on $($computer)!"
                Write-Verbose "Group '$($GroupName)' not found on $($computer)!"
          }
       } Catch {
          $PSCmdlet.WriteError($_)
       }
    }

   $ComputerName | ForEach-Object {
      $computer = $_
      Write-Verbose "Processing $($computer) ..."
        Get-Members $Group | Sort-Object -Property GroupName, Member
   }
}


Open in new window


Regards,

Rob.
Get an unlimited membership to EE for less than $4 a week.
Unlimited question asking, solutions, articles and more.
Bill Purvis

ASKER
Hi Rob,
You are right on the money!!  Two things I hope you can do for this script is add a "Computers" link in it so I can add only the servers we have an Audit on and Output to a .csv file?

Bill
RobSampson

Hi Bill.  The script can still be run the same way that oBdA mentioned, using the command line to pass the parameters for the computerlist.txt file and the CSV output
Get-Content .\ComputerList.txt | .\Get-LocalGroupMember.ps1 -Verbose | Export-Csv -NoTypeInformation -Path .\members.csv

Open in new window

That should work for you.

Rob.
Bill Purvis

ASKER
Getting an error:

At E:\Bill\~PowerShell\LocalAdminNested\LocalAdminsNested.ps1:2 char:1
+ [CmdletBinding()]
+ ~~~~~~~~~~~~~~~~~
Unexpected attribute 'CmdletBinding'.
At E:\Bill\~PowerShell\LocalAdminNested\LocalAdminsNested.ps1:3 char:1
+ Param (
+ ~~~~~
Unexpected token 'Param' in expression or statement.
At E:\Bill\~PowerShell\LocalAdminNested\LocalAdminsNested.ps1:5 char:21
+    [string]$Group = 'Administrators',
+                     ~~~~~~~~~~~~~~~~
The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a variable or a property.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedAttribute

Open in new window

Your help has saved me hundreds of hours of internet surfing.
fblack61
RobSampson

Hmmm. Interesting.  Can you make sure you have used the Select all button under the code snippet, then pressed CTRL+C and paste it into your script with CTRL+V?  I have done just that, and it works when I run
Get-Content .\ComputerList.txt | .\LocalAdminsNested.ps1 -Verbose | Export-Csv -NoTypeInformation -Path .\members.c

Open in new window

For that command to work exactly as is, you will need to be in the
E:\Bill\~PowerShell\LocalAdminNested\
folder in the PowerShell prompt, and also have ComputerList.txt and LocalAdminsNested.ps1 in that same folder.
ComputerList.txt should have one computer name per line.

Judging by the error you have, I suspect there may be some invalid character near the start of the script that stops the CmdletBinding from working properly.

Rob.
Bill Purvis

ASKER
Here's what I got:


PS E:\bill\~PowerShell\LocalAdminNested> E:\Bill\~PowerShell\LocalAdminNested\LocalAdminsNested.ps1
At E:\Bill\~PowerShell\LocalAdminNested\LocalAdminsNested.ps1:2 char:1
+ [CmdletBinding()]
+ ~~~~~~~~~~~~~~~~~
Unexpected attribute 'CmdletBinding'.
At E:\Bill\~PowerShell\LocalAdminNested\LocalAdminsNested.ps1:3 char:1
+ Param (
+ ~~~~~
Unexpected token 'Param' in expression or statement.
At E:\Bill\~PowerShell\LocalAdminNested\LocalAdminsNested.ps1:5 char:21
+    [string]$Group = 'Administrators',
+                     ~~~~~~~~~~~~~~~~
The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a variable or a property.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedAttribute

Open in new window

ASKER CERTIFIED SOLUTION
RobSampson

Log in or sign up to see answer
Become an EE member today7-DAY FREE TRIAL
Members can start a 7-Day Free trial then enjoy unlimited access to the platform
Sign up - Free for 7 days
or
Learn why we charge membership fees
We get it - no one likes a content blocker. Take one extra minute and find out why we block content.
Not exactly the question you had in mind?
Sign up for an EE membership and get your own personalized solution. With an EE membership, you can ask unlimited troubleshooting, research, or opinion questions.
ask a question
Bill Purvis

ASKER
Rob,
  You are a genius!!  Thank you very much.  So thankful you are still out there!

Bill 
Get an unlimited membership to EE for less than $4 a week.
Unlimited question asking, solutions, articles and more.