{"id":2653,"date":"2013-10-27T00:01:00","date_gmt":"2013-10-27T00:01:00","guid":{"rendered":"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2013\/10\/27\/the-admins-first-steps-local-group-membership\/"},"modified":"2013-10-27T00:01:00","modified_gmt":"2013-10-27T00:01:00","slug":"the-admins-first-steps-local-group-membership","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/scripting\/the-admins-first-steps-local-group-membership\/","title":{"rendered":"The Admin&#8217;s First Steps: Local Group Membership"},"content":{"rendered":"<p><strong>Summary<\/strong>: Richard Siddaway talks about using Windows PowerShell to discover the membership of local groups.\n<img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/q-for-powertip.jpg\" alt=\"Hey, Scripting Guy! Question\">&nbsp;Hey, Scripting Guy! I&rsquo;ve just starting using Windows PowerShell to administer my systems, and I&rsquo;ve been told I need to check the membership of local groups on all my servers. How can I do that?\n&mdash; AK\n<img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/wp-content\/uploads\/sites\/29\/2019\/02\/a-for-powertip.jpg\" alt=\"Hey, Scripting Guy! Answer\">&nbsp;Hello AK,\nHonorary Scripting Guy, Richard Siddaway here today&mdash;filling in for my good friend, The Scripting Guy. Windows PowerShell is great because you have a number of ways to perform most tasks. You&rsquo;ll see an example of that in this post, where there are three ways to solve this issue. This is a strength but also a weakness because it can confuse people who are starting to use Windows PowerShell. My advice has always been to find something that works and stick with it. You get more issues solved that way.\nWhen we look at local groups, the most important is the Administrators group. This bestows full control of the system and allows you to do anything to that machine. We&rsquo;ll use that group as an example throughout this post, but the ideas can be applied to any local group.\nOf the three ways to find local group membership, the oldest method uses the WinNT ADSI provider. ADSI is a scripting interface to directory services. It&rsquo;s normally used for scripting against Active Directory, but you can also use it against local machines.<\/p>\n<p style=\"padding-left: 30px\">$group = [ADSI]&#8221;WinNT:\/\/.\/Administrators&#8221;<\/p>\n<p style=\"padding-left: 30px\">&nbsp;@($group.Invoke(&#8220;Members&#8221;)) |<\/p>\n<p style=\"padding-left: 30px\">foreach {$_.GetType().InvokeMember(&#8220;Name&#8221;, &#8216;GetProperty&#8217;, $null, $_, $null)}\nThe ADSI scripting interface isn&rsquo;t directly available in Windows PowerShell in the same way it is in VBScript. Instead, you have to use the System.DirectoryServices.DirectoryEntry .NET Framework class, which provides a wrapper for ADSI. A shortcut, known as a type accelerator has been provided so you don&rsquo;t have to type the whole class name. To add confusion, the type accelerator is known as <strong>[adsi]<\/strong> or <strong>[ADSI]<\/strong>&mdash;case doesn&rsquo;t matter.\nThe WinNT provider is used to access the local machine by using &ldquo;.&rdquo; to represent the system together with the name of the group&mdash;in this case, Administrators. You then have to use the <strong>Invoke()<\/strong> method of the <strong>$group<\/strong> object to get the members that you can pipe to <strong>Foreach-Object<\/strong>, which uses the <strong>InvokeMember()<\/strong> method to get the member name. The output will look something like this:<\/p>\n<p style=\"padding-left: 30px\">Administrator<\/p>\n<p style=\"padding-left: 30px\">Richard\nPersonally, I don&rsquo;t like using &ldquo;.&rdquo; to represent the local machine. It&rsquo;s easy to overlook, and I have seen problems when using it with WMI. I prefer to use <strong>$env:COMPUTERNAME<\/strong>, and pick the machine&rsquo;s name from the environmental variables:<\/p>\n<p style=\"padding-left: 30px\">$group =[ADSI]&#8221;WinNT:\/\/$($env:COMPUTERNAME)\/Administrators&#8221;<\/p>\n<p style=\"padding-left: 30px\">&nbsp;@($group.Invoke(&#8220;Members&#8221;)) |<\/p>\n<p style=\"padding-left: 30px\">foreach {$_.GetType().InvokeMember(&#8220;Name&#8221;, &#8216;GetProperty&#8217;, $null, $_, $null)}\nThis is OK if your machine is in a workgroup; but in a domain, you might get answers like this:<\/p>\n<p style=\"padding-left: 30px\">Administrator<\/p>\n<p style=\"padding-left: 30px\">Domain Admins<\/p>\n<p style=\"padding-left: 30px\">Richard\nIt&rsquo;s not easy to separate local users from domain accounts by using this approach. Luckily, you have some alternatives. One of the alternatives involves using WMI. When I&rsquo;m working with WMI, I find that the CIM cmdlets introduced in Windows PowerShell&nbsp;3.0 are the easiest to work with:<\/p>\n<p style=\"padding-left: 30px\">$group = Get-CimInstance -ClassName Win32_Group&nbsp; -Filter &#8220;Name = &#8216;Administrators'&#8221;<\/p>\n<p style=\"padding-left: 30px\">Get-CimAssociatedInstance -InputObject $group -ResultClassName Win32_UserAccount |<\/p>\n<p style=\"padding-left: 30px\">select -ExpandProperty Caption\nUse the <strong>Win32_Group<\/strong> class to get the WMI object that represents the group. Use that object in <strong>Get-CimAssociatedInstance<\/strong> to find the <strong>Win32_UserAccount<\/strong> instances that are associated with that group. On a workgroup machine, you get this:<\/p>\n<p style=\"padding-left: 30px\">RSLAPTOP01Administrator<\/p>\n<p style=\"padding-left: 30px\">RSLAPTOP01Richard\nAnd on a domain machine, you get this:<\/p>\n<p style=\"padding-left: 30px\">WIN12R2Administrator<\/p>\n<p style=\"padding-left: 30px\">MANTICORERichard\nNow you can see which of the results are local accounts and which are domain accounts. But (and isn&rsquo;t there always a but?) you don&rsquo;t get the nested domain groups. You need to find the associated groups, which simply involves another call to <strong>Get-CimAssociatedInstance<\/strong>:<\/p>\n<p style=\"padding-left: 30px\">$group = Get-CimInstance -ClassName Win32_Group&nbsp; -Filter &#8220;Name = &#8216;Administrators'&#8221;<\/p>\n<p style=\"padding-left: 30px\">Get-CimAssociatedInstance -InputObject $group -ResultClassName Win32_UserAccount |<\/p>\n<p style=\"padding-left: 30px\">select -ExpandProperty Caption<\/p>\n<p style=\"padding-left: 30px\">Get-CimAssociatedInstance -InputObject $group -ResultClassName Win32_Group |<\/p>\n<p style=\"padding-left: 30px\">select -ExpandProperty Caption\nBecause you are displaying the same data, you can output from multiple calls (generally its viewed as a bad thing), and you get something like this:<\/p>\n<p style=\"padding-left: 30px\">WIN12R2Administrator<\/p>\n<p style=\"padding-left: 30px\">MANTICORERichard<\/p>\n<p style=\"padding-left: 30px\">MANTICOREDomain Admins\nNow you can see the domain and machine name, so you know which entries are local, and you get the nested groups. If you can&rsquo;t use the CIM cmdlets, you can fall back on the WMI cmdlets.<\/p>\n<p style=\"padding-left: 30px\">$query = &#8220;ASSOCIATORS OF {Win32_Group.Domain=&#8217;$($env:COMPUTERNAME)&#8217;,Name=&#8217;Administrators&#8217;} WHERE ResultClass = Win32_UserAccount&#8221;<\/p>\n<p style=\"padding-left: 30px\">Get-WmiObject -Query $query | Select -ExpandProperty Caption<\/p>\n<p style=\"padding-left: 30px\">$query = &#8220;ASSOCIATORS OF {Win32_Group.Domain=&#8217;$($env:COMPUTERNAME)&#8217;,Name=&#8217;Administrators&#8217;} WHERE ResultClass = Win32_Group&#8221;<\/p>\n<p style=\"padding-left: 30px\">Get-WmiObject -Query $query | Select -ExpandProperty Caption\nThis is a bit more complicated than the CIM cmdlet version because you have to create a WMI Query Language (WQL) query to discover the associated <strong>Win32_UserAccount<\/strong> and <strong>Win32_Group<\/strong> instances. The difficult part is getting the contents of the <strong>{}<\/strong> correct. The easiest way is to look at the <strong>__RELPATH<\/strong> property of the returned object when you use the following command:<\/p>\n<p style=\"padding-left: 30px\">Get-WmiObject -Class Win32_Group -Filter &#8220;Name = &#8216;Administrators'&#8221; | fl *\nRemember to change the double quotes to single quotes to make the query work.\nYou get the same results as with the CIM cmdlets:<\/p>\n<p style=\"padding-left: 30px\">WIN12R2Administrator<\/p>\n<p style=\"padding-left: 30px\">MANTICORERichard<\/p>\n<p style=\"padding-left: 30px\">MANTICOREDomain Admins\nThe third and final way to get this data is to drop back to the .NET Framework class and use the <strong>System.DirectoryServices.AccountManagement<\/strong> classes that were introduced in .NET Framework&nbsp;3.5.<\/p>\n<p style=\"padding-left: 30px\">Add-Type -AssemblyName System.DirectoryServices.AccountManagement<\/p>\n<p style=\"padding-left: 30px\">$ctype = [System.DirectoryServices.AccountManagement.ContextType]::Machine<\/p>\n<p style=\"padding-left: 30px\">$context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ctype, $env:COMPUTERNAME<\/p>\n<p style=\"padding-left: 30px\">$idtype = [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName<\/p>\n<p style=\"padding-left: 30px\">$group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($context, $idtype, &#8216;Administrators&#8217;)<\/p>\n<p style=\"padding-left: 30px\">$group.Members |<\/p>\n<p style=\"padding-left: 30px\">select @{N=&#8217;Domain&#8217;; E={$_.Context.Name}}, samaccountName\nThis may look a bit more complicated, but most of it is concerned with defining the objects that you want to work with. The script starts by using <strong>Add-Type<\/strong> to load the <strong>System.DirectoryServices.AccountManagement<\/strong> .NET Framework classes into Windows PowerShell. Not all.NET Framework classes are loaded by default&mdash;only those that are thought to be of the most use to the most people.\nAfter that, you use the <strong>ContextType<\/strong> class to say that you are looking at the local machine rather than the domain. The <strong>System.DirectoryServices.AccountManagement<\/strong> classes aren&rsquo;t used much, which is a shame because they are powerful and they span local and domain level activities.\nYou can then use the context type and the local machine name to create a context. The <strong>IdentityType<\/strong> defines what you are using to identify objects&mdash;in this case, the <strong>SamAccountName<\/strong>.\nNow you can use all of that data in the <strong>FindByIdentity()<\/strong> method of the <strong>GroupPrincipal<\/strong> class to find the group. The <strong>Members<\/strong> property holds the members as you might expect; but the final piece is that you have to dig into the Context of the member to discover whether the object comes from the domain or from the local machine.\nLuckily the code runs much faster than I can write about it and you get results like this:<\/p>\n<p style=\"padding-left: 30px\">Domain&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SamAccountName<\/p>\n<p style=\"padding-left: 30px\">&#8212;&#8212;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8212;&#8212;&#8212;&#8212;&#8211;<\/p>\n<p style=\"padding-left: 30px\">WIN12R2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;Administrator<\/p>\n<p style=\"padding-left: 30px\">Manticore.org &nbsp;&nbsp;Domain Admins<\/p>\n<p style=\"padding-left: 30px\">Manticore.org &nbsp;&nbsp;Richard&nbsp;\nWorking through these three methods illustrates an important point: If there are multiple methods available, you should investigate them to discover how they work, any issues that occur for you, and which one works best with your skills and way of working.\nThe last part of this journey is to turn your code into a function that you can use across multiple remote machines. I&rsquo;m going to use the <strong>System.DirectoryServices.AccountManagement<\/strong> approach so I can illustrate how these classes can be used against remote machines without the need to rely on Windows PowerShell remoting.<\/p>\n<p style=\"padding-left: 30px\">function get-localgroupmember {<\/p>\n<p style=\"padding-left: 30px\">[CmdletBinding()]<\/p>\n<p style=\"padding-left: 30px\">param(<\/p>\n<p style=\"padding-left: 30px\">[parameter(ValueFromPipeline=$true,<\/p>\n<p style=\"padding-left: 30px\">&nbsp;&nbsp; ValueFromPipelineByPropertyName=$true)]<\/p>\n<p style=\"padding-left: 30px\">&nbsp;&nbsp; [string[]]$computername = $env:COMPUTERNAME<\/p>\n<p style=\"padding-left: 30px\">)<\/p>\n<p style=\"padding-left: 30px\">BEGIN {<\/p>\n<p style=\"padding-left: 30px\">Add-Type -AssemblyName System.DirectoryServices.AccountManagement<\/p>\n<p style=\"padding-left: 30px\">$ctype = [System.DirectoryServices.AccountManagement.ContextType]::Machine<\/p>\n<p style=\"padding-left: 30px\">}<\/p>\n<p style=\"padding-left: 30px\">&nbsp;<\/p>\n<p style=\"padding-left: 30px\">PROCESS{<\/p>\n<p style=\"padding-left: 30px\">foreach ($computer in $computername) {<\/p>\n<p style=\"padding-left: 30px\">&nbsp; $context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ctype, $computer<\/p>\n<p style=\"padding-left: 30px\">&nbsp; $idtype = [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName<\/p>\n<p style=\"padding-left: 30px\">&nbsp; $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($context, $idtype, &#8216;Administrators&#8217;)<\/p>\n<p style=\"padding-left: 30px\">&nbsp; $group.Members |<\/p>\n<p style=\"padding-left: 30px\">&nbsp; select @{N=&#8217;Server&#8217;; E={$computer}}, @{N=&#8217;Domain&#8217;; E={$_.Context.Name}}, samaccountName<\/p>\n<p style=\"padding-left: 30px\">} # end foreach<\/p>\n<p style=\"padding-left: 30px\">} # end PROCESS<\/p>\n<p style=\"padding-left: 30px\">}<\/p>\n<p style=\"padding-left: 30px\">&nbsp;<\/p>\n<p style=\"padding-left: 30px\">&#8220;Win12R2&#8221;, &#8220;W12SUS&#8221; | get-localgroupmember\nStart by defining the function and parameter. In this case, you&rsquo;re only interested in the remote computer name. When you use an array as the type, you can also do this:<\/p>\n<p style=\"padding-left: 30px\">get-localgroupmember -computername Win12R2, w12sus\n&hellip;but only if you have the <strong>Foreach<\/strong> loop in the PROCESS block. (You don&rsquo;t have to capitalize these blocks of code&mdash;I do it to make my code more readable.)\nYou only need to load the .NET Framework classes, define the context, and identity types once. So those steps go into the BEGIN block, which executes once when the first object in the pipeline hits the function.\nThe PROCESS block loops through the computers and creates a context for each machine. The Administrators group is found and the membership list is extracted. You are working against remote machines so the computer is added to the output to identify to which machine the results apply.\nYou can use this function on the pipeline as shown or interactively by passing one or more computer names to the function.\nIf you want to extend the use of this script you could:<\/p>\n<ul>\n<li>Make the group name a variable.<\/li>\n<li>Save the results for comparison against a future examination to track changes.<\/li>\n<\/ul>\n<p>AK, that&rsquo;s how you use Windows PowerShell to check the membership of your local groups. Next time I&rsquo;ll have another idea for you to try as you bring more automation into your environment.\nIf you would like to read more in this series, check out these posts:<\/p>\n<ul>\n<li><a href=\"http:\/\/blogs.technet.com\/b\/heyscriptingguy\/archive\/2013\/09\/25\/the-admin-s-first-steps-documenting-servers.aspx\" target=\"_blank\">The Admin&rsquo;s First Steps: Documenting Servers<\/a><\/li>\n<li><a href=\"http:\/\/blogs.technet.com\/b\/heyscriptingguy\/archive\/2013\/10\/02\/the-admin-s-first-steps-scan-multiple-event-logs.aspx\" target=\"_blank\">The Admin&rsquo;s First Steps: Scan Multiple Event Logs<\/a><\/li>\n<li><a href=\"http:\/\/blogs.technet.com\/controlpanel\/blogs\/posteditor.aspx?SelectedNavItem=Posts&amp;WeblogID=7618&amp;WeblogPostID=3600971\" target=\"_blank\">The Admin&rsquo;s First Steps: Testing Service Health<\/a><\/li>\n<li><a href=\"http:\/\/blogs.technet.com\/b\/heyscriptingguy\/archive\/2013\/10\/13\/the-admin-s-first-steps-discovering-shares.aspx\" target=\"_blank\">The Admin&rsquo;s First Steps: Discovering Shares<\/a><\/li>\n<li><a href=\"http:\/\/blogs.technet.com\/controlpanel\/blogs\/posteditor.aspx?SelectedNavItem=Posts&amp;WeblogID=7618&amp;WeblogPostID=3602009\" target=\"_blank\">The Admin&rsquo;s First Steps: Empty Groups<\/a><\/li>\n<\/ul>\n<p>Bye for now.\n~Richard<\/p>\n<p style=\"padding-left: 30px\">Richard Siddaway is based out of the UK, and he spends his time automating anything and everything for Kelway, Ltd. A Windows PowerShell MVP since 2007, Richard is a prolific blogger, mainly about Windows PowerShell (see&nbsp;<a href=\"http:\/\/msmvps.com\/blogs\/richardsiddaway\/default.aspx\" target=\"_blank\">Richard Siddaway&#8217;s Blog: Of PowerShell and Other Things<\/a>), and a frequent speaker at user groups and Windows PowerShell conferences. He has written a number of Windows PowerShell books: PowerShell in Practice, PowerShell and WMI, PowerShell in Depth (co-author); and PowerShell Dive (co-editor). He is currently finishing Learn Active Directory Management in a Month of Lunches, which features a lot of Windows PowerShell. All of the books are available from&nbsp;<a href=\"http:\/\/www.manning.com\/search\/results?cx=008207406337866288189%3Avej9zumcdec&amp;cof=FORID%3A9&amp;ie=UTF-8&amp;q=PowerShell+in+Practice&amp;sa=Search\" target=\"_blank\">Manning Publications Co.<\/a>\nThanks, Richard.\nI invite you to follow me on&nbsp;<a href=\"http:\/\/bit.ly\/scriptingguystwitter\" target=\"_blank\">Twitter<\/a>&nbsp;and&nbsp;<a href=\"http:\/\/bit.ly\/scriptingguysfacebook\" target=\"_blank\">Facebook<\/a>. If you have any questions, send email to me at&nbsp;<a href=\"http:\/\/blogs.technet.commailto:scripter@microsoft.com\" target=\"_blank\">scripter@microsoft.com<\/a>, or post your questions on the&nbsp;<a href=\"http:\/\/bit.ly\/scriptingforum\" target=\"_blank\">Official Scripting Guys Forum<\/a>. See you tomorrow. Until then, peace.\n<strong>Ed Wilson<\/strong>, Microsoft Scripting Guy<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Summary: Richard Siddaway talks about using Windows PowerShell to discover the membership of local groups. &nbsp;Hey, Scripting Guy! I&rsquo;ve just starting using Windows PowerShell to administer my systems, and I&rsquo;ve been told I need to check the membership of local groups on all my servers. How can I do that? &mdash; AK &nbsp;Hello AK, Honorary [&hellip;]<\/p>\n","protected":false},"author":596,"featured_media":87096,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[453,51,44,56,197,189,3,4,45],"class_list":["post-2653","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-scripting","tag-admin-first-steps","tag-getting-started","tag-groups","tag-guest-blogger","tag-local-account-management","tag-richard-siddaway","tag-scripting-guy","tag-scripting-techniques","tag-windows-powershell"],"acf":[],"blog_post_summary":"<p>Summary: Richard Siddaway talks about using Windows PowerShell to discover the membership of local groups. &nbsp;Hey, Scripting Guy! I&rsquo;ve just starting using Windows PowerShell to administer my systems, and I&rsquo;ve been told I need to check the membership of local groups on all my servers. How can I do that? &mdash; AK &nbsp;Hello AK, Honorary [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/2653","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/users\/596"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/comments?post=2653"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/2653\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media\/87096"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/media?parent=2653"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/categories?post=2653"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/tags?post=2653"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}