Get Local Member

I’ve been dealing with the topic of enumerating local group membership in several PowerShell forums from different people. My responses have pretty much been the same. But if these people are asking its a safe bet there are others who are also in need. The bottom line has been to query a local group, typically Administrators, and return a list of group members. If the member is a domain account, then get a little extra information. I put together a script called Get-Localmember.ps1 that I think will get the job done for most of you.

#Get-Localmember.ps1
Param(  
    [string]$computername=$env:Computername,
    [string]$group="Administrators"
    )
      
    Function Get-DomainUser {
        Param([string]$sam="Administrator")
        
        $searcher=New-Object system.DirectoryServices.DirectorySearcher
        $searcher.PageSize=100
        $searcher.filter="samaccountname=$sam"
        $user=$searcher.findone()
        write $user
    
 }
 
$errorActionPreference="SilentlyContinue"
New-Variable ADS_UF_ACCOUNTDISABLE 0x0002 -Option Constant
 
trap {
   Write-Warning ("Oops.  Something happened trying to return group membership for {0} on {1}." -f $group,$computername.ToUpper())
}
 
[ADSI]$LocalGroup="WinNT://$computername/$group,group"
 
$LocalGroup.psbase.invoke("Members") | ForEach-Object {
 
    #get ADS Path of member
    $ADSPath=$_.GetType().InvokeMember("ADSPath", 'GetProperty', `
    $null, $_, $null)
   
    #get the member class, ie user or group
    $class=$_.GetType().InvokeMember("Class", 'GetProperty', `
    $null, $_, $null)
   
    #Get the name property
    $name=$_.GetType().InvokeMember("Name", 'GetProperty', `
    $null, $_, $null)
   
    #if computer name is found between two /, then assume
    #the ADSPath reflects a local object
    if ($ADSPath -match "$computername") 
    {
        $local=$True
        $domain=$computername.ToUpper()
        $description=$_.GetType().InvokeMember("Description", 'GetProperty', `
    $null, $_, $null)
        $displayname=$_.GetType().InvokeMember("FullName", 'GetProperty', `
    $null, $_, $null)
         $account=$ADSPath
         $flag=$_.GetType().InvokeMember("userflags", 'GetProperty', `
    $null, $_, $null)
          if ($flag -band $ADS_UF_ACCOUNTDISABLE) 
          {
            $disabled=$True
         }
         else 
         {
            $disabled=$False
         }
          
     } #end if $ADSPath -match $computername
    else  #account is a domain member
    {
        $local=$False
        #Domain members will have an ADSPath like
         #WinNT://MYDomain/Domain Users.  Local accounts will have
        #be like WinNT://MYDomain/Computername/Administrator
    
        #using regular expressions create a named match for the domain name
        $ADSPath -match "(?<domain>//\w+)" | Out-Null
    
        #strip off the leading //
        $domain=$matches.domain.Replace("//","")
        $domainuser=Get-DomainUser $name
        if ($domainuser) 
        {
            $description=$domainuser.properties.item("description")[0]
            $displayname=$domainuser.properties.item("displayname")[0]
            $account=$domainuser.properties.item("distinguishedname")[0]
 
            if ($domainuser.properties.item("useraccountcontrol")[0] -band $ADS_UF_ACCOUNTDISABLE ) 
              {
                $disabled=$True
             }
             else 
             {
                $disabled=$False
             }
        } #end if $domainuser
       else 
       {
           $description="not found"
           $displayname="not found"
           $disabled=$null
           $account=$ADSPath
       }
   } #end else domain user
   
    #create a custom object
    $obj = New-Object PSObject
   
    #define custom object properties
    $obj | Add-Member -MemberType NoteProperty -Name "Computer" -Value $computername.toUpper()
    $obj | Add-Member -MemberType NoteProperty -Name "Account" -Value $account
    $obj | Add-Member -MemberType NoteProperty -Name "Name" -Value $name
    $obj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $displayname
    $obj | Add-Member -MemberType NoteProperty -Name "Description" -Value $description
    $obj | Add-Member -MemberType NoteProperty -Name "Disabled" -Value $disabled
    $obj | Add-Member -MemberType NoteProperty -Name "Domain" -Value $domain
    $obj | Add-Member -MemberType NoteProperty -Name "IsLocal" -Value $local
    $obj | Add-Member -MemberType NoteProperty -Name "Class" -Value $class
   
    #write the result to the pipeline
    write $obj
   
 #end foreach
 

The script takes computer and group name’s as parameters. The defaults are the local computer and the Administrator’s group. Let me first give you a sample of the script’s output, which is written to the pipeline:\

Computer    : XP01
Account     : WinNT://MYCOMPANY/XP01/svcaccount
Name        : svcaccount
DisplayName :
Description : test service acount
Disabled    : False
Domain      : XP01
IsLocal     : True
Class       : User

Computer    : XP01
Account     : CN=Domain Admins,CN=Users,DC=MYCOMPANY,DC=LOCAL
Name        : Domain Admins
DisplayName :
Description : Designated administrators of the domain
Disabled    : False
Domain      : MYCOMPANY
IsLocal     : False
Class       : group

Computer    : XP01
Account     : CN=Jeffery Hicks,OU=IT,OU=Employees,DC=MYCOMPANY,DC=LOCAL
Name        : jhicks
DisplayName : Jeffery Hicks
Description : Company admin
Disabled    : False
Domain      : MYCOMPANY
IsLocal     : False
Class       : User

A custom object is created for each group member which means you can filter, sort, export or whatever you want with the results. Here’s how this works. First the script defines some variables and a trap to handle any errors.

$errorActionPreference="SilentlyContinue"
New-Variable ADS_UF_ACCOUNTDISABLE 0x0002 -Option Constant
 
trap {
   Write-Warning ("Oops.  Something happened trying to return group membership for {0} on {1}." -f $group,$computername.ToUpper())
}

The $ADS_UF_ACCOUNTDISABLE variable will be used later to determine if the account is disabled or not. Using the [ADSI] type adapter, the script connects to the specified group object on the specified computer.

#connect to the specified group and create an ADSI object
#Remember WinNT is case-sensitive
[ADSI]$LocalGroup="WinNT://$computername/$group,group"

I now need to process each group member.

#enumerate group members and for each one get information
#and create a custom object.
$LocalGroup.psbase.invoke("Members") | ForEach-Object {

Getting properties for local accounts in PowerShell is not as straightforward as you might like due to limitations in the underlying .NET class and how PowerShell adapts the class.  But no matter, here is how I get some of the properties I’m interested in.

#get ADS Path of member
$ADSPath=$_.GetType().InvokeMember("ADSPath", 'GetProperty', `
$null, $_, $null)
 
#get the member class, ie user or group
$class=$_.GetType().InvokeMember("Class", 'GetProperty', `
$null, $_, $null)
 
#Get the name property
$name=$_.GetType().InvokeMember("Name", 'GetProperty', `
$null, $_, $null)

The ADSPath is used to determine if the account is a domain member or local. Domain members will have an ADSPath like WinNT://MYDomain/Domain Users.  Local accounts will have a value like WinNT://MYDomain/Computername/Administrator. With this information if the account is local then I’ll get local properties.

#if computer name is found between two /, then assume
#the ADSPath reflects a local object
if ($ADSPath -match "$computername") 
{
  $local=$True
  $domain=$computername.ToUpper()
  $description=$_.GetType().InvokeMember("Description", 'GetProperty', `
$null, $_, $null)
  $displayname=$_.GetType().InvokeMember("FullName", 'GetProperty', `
$null, $_, $null)
$account=$ADSPath
$flag=$_.GetType().InvokeMember("userflags", 'GetProperty', `
$null, $_, $null)
 if ($flag -band $ADS_UF_ACCOUNTDISABLE) 
 {
   $disabled=$True
}
else 
{
   $disabled=$False
}
 
} #end if $ADSPath -match $computername

I want to point out how I determine if the user account is disabled or not. I have to retrieve the user account control value stored in the userflags property and then perform a binary AND comparison with the $ADS_UF_ACCOUNTDISABLE variable. If the result is true then the account is disabled.

$flag=$_.GetType().InvokeMember("userflags", 'GetProperty', `
, $_, $null)
 if ($flag -band $ADS_UF_ACCOUNTDISABLE) 
 {
   $disabled=$True
}
else 
{
   $disabled=$False
}

If the account is a domain member then the first thing I want to do is parse out the domain name from the ADSPath using a regular expression pattern with a named match, “<domain>”. It’s not the most robust expression because it assumes you don’t have any numbers in your domain name.

else  #account is a domain member
{
    $local=$False
    #Domain members will have an ADSPath like
     #WinNT://MYDomain/Domain Users.  Local accounts will 
    #be like WinNT://MYDomain/Computername/Administrator
 
    #using regular expressions create a named match for the domain name
    $ADSPath -match "(?<domain>//\w+)" | Out-Null
 
    #strip off the leading //
    $domain=$matches.domain.Replace("//","")

In order to get information about the user (or group) such as display name, description or distinguishedname, I need to find the object in Active Directory. I pass the member’s SAMAccountname to a Get-DomainUser function.

Function Get-DomainUser {
    Param([string]$sam="Administrator")
        
    $searcher=New-Object system.DirectoryServices.DirectorySearcher
    $searcher.PageSize=100
    $searcher.filter="samaccountname=$sam"
    $user=$searcher.findone()
    write $user
}

Using a DirectorySearcher, the function finds the first object that matches the SAMaccountname, which should be unique.  I have not tested how this will work in a cross-domain situation. Unless your local group has hundreds of domain members, performance shouldn’t be too bad. Simply remember that Active Directory must be queried for each domain member. The function returns the search result object to the pipeline. Also, be aware that this is not the actual object in Active Directory but a search result object so it won’t have every single property, but it should have enough of the basics to meet your needs. Assuming I get a result back, I’ll grab some Active Directory properties.

$domainuser=Get-DomainUser $name
if ($domainuser) 
{
    $description=$domainuser.properties.item("description")[0]
    $displayname=$domainuser.properties.item("displayname")[0]
    $account=$domainuser.properties.item("distinguishedname")[0]

Determining if the account is disabled is basically the same process. The location of the user account flag is slightly different.

if ($domainuser.properties.item("useraccountcontrol")[0] -band $ADS_UF_ACCOUNTDISABLE ) 
 {
    $disabled=$True
 }
 else 
 {
    $disabled=$False
 }

At this point, all that is left is to build a custom object and add my properties. This is repeated for every group member.

#create a custom object
$obj = New-Object PSObject
 
#define custom object properties
$obj | Add-Member -MemberType NoteProperty -Name "Computer" -Value $computername.toUpper()
$obj | Add-Member -MemberType NoteProperty -Name "Account" -Value $account
$obj | Add-Member -MemberType NoteProperty -Name "Name" -Value $name
$obj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $displayname
$obj | Add-Member -MemberType NoteProperty -Name "Description" -Value $description
$obj | Add-Member -MemberType NoteProperty -Name "Disabled" -Value $disabled
$obj | Add-Member -MemberType NoteProperty -Name "Domain" -Value $domain
$obj | Add-Member -MemberType NoteProperty -Name "IsLocal" -Value $local
$obj | Add-Member -MemberType NoteProperty -Name "Class" -Value $class
 
#write the result to the pipeline
write $obj

Download this script file here.

Look for a graphical variation on this script in a future Mr. Roboto column.