{"id":2696,"date":"2013-10-20T00:01:00","date_gmt":"2013-10-20T00:01:00","guid":{"rendered":"https:\/\/blogs.technet.microsoft.com\/heyscriptingguy\/2013\/10\/20\/the-admins-first-steps-empty-groups\/"},"modified":"2013-10-20T00:01:00","modified_gmt":"2013-10-20T00:01:00","slug":"the-admins-first-steps-empty-groups","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/scripting\/the-admins-first-steps-empty-groups\/","title":{"rendered":"The Admin&#8217;s First Steps: Empty Groups"},"content":{"rendered":"<p><strong>Summary<\/strong>: Richard Siddaway talks about using Windows PowerShell to discover Active Directory groups that have no members.\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 audit our Active Directory to discover any empty groups. How can I do that?\n&mdash;TL\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 TL,\nHonorary Scripting Guy, Richard Siddaway here today&mdash;filling in for my good friend, The Scripting Guy. You&rsquo;re in luck because today I&rsquo;ve got a solution to that issue as part of my series of posts about how an administrator can start making productive use of Windows PowerShell.\nI&rsquo;m a big fan of doing regular audits of your Active Directory. It keeps it clean and reduces problems in the long run. I don&rsquo;t want to count the number of hours I&rsquo;ve spent cleaning up Active Directory environments where there&rsquo;s been no housekeeping for years. There are a number of things you should consider when auditing Active Directory, including:<\/p>\n<ul>\n<li>Old user accounts<\/li>\n<li>Old computer accounts<\/li>\n<li>Empty groups, or groups with very few users<\/li>\n<li>Membership of sensitive groups such as Enterprise Admins or Domain Admins<\/li>\n<\/ul>\n<p>In this post, we&rsquo;ll look at group membership.\nYou have four ways to work with Active Directory from Windows PowerShell:<\/p>\n<ol>\n<li>Windows PowerShell cmdlets (introduced in Windows Server&nbsp;2008&nbsp;R2)<\/li>\n<li>.NET classes in the System.DirectoryServices namespace<\/li>\n<li>Active Directory provider that comes with the Windows PowerShell cmdlets<\/li>\n<li>Dell (Quest) cmdlets<\/li>\n<\/ol>\n<p>My preference these days is to use the Windows PowerShell cmdlets or scripting with the .NET classes. You can do most things through the Active Directory provider, but it&rsquo;s a more cumbersome and difficult that way. The Quest cmdlets are good, but with Windows PowerShell cmdlets becoming more widely available as organizations move to Windows Server&nbsp;2008&nbsp;R2 or later for their domain controllers, I prefer to use the native tools.\nThe Windows PowerShell cmdlets are available in the Active Directory module. This is available on any computer running Windows Server&nbsp;2008&nbsp;R2 or later that is a domain controller or has the Active Directory RSAT tools installed. If you are running Windows PowerShell&nbsp;4.0 or Windows PowerShell&nbsp;3.0, this module is automatically loaded for you. Otherwise, you need to import it as follows:<\/p>\n<p style=\"padding-left: 30px\">Import-Module ActiveDirectory\nThe first step is to find the groups in your environment. Assuming that you want to test the groups in the whole domain, use:<\/p>\n<p style=\"padding-left: 30px\">Get-ADGroup -Filter * | Select Name, DistinguishedName\nIf you want to restrict this process to a specific OU tree, you can be more granular in your approach:<\/p>\n<p style=\"padding-left: 30px\">Get-ADGroup -Filter * -SearchBase &#8220;OU=All Groups,DC=Manticore,DC=org&#8221; -SearchScope Subtree | Select Name, DistinguishedName\nIf you haven&rsquo;t performed a cleanup for a long time, a more granular approach may be best unless you keep all of your groups in one OU.\nNow that you know the groups you want to work with, you need to find the members of those groups. The <strong>Get-ADGroupMember<\/strong> cmdlet performs this task. Unfortunately, it doesn&rsquo;t work with the group name. You need to use one of the following:<\/p>\n<ul>\n<li>SamAccountName<\/li>\n<li>Distinguished name<\/li>\n<li>GUI<\/li>\n<li>SID<\/li>\n<\/ul>\n<p>As an extra awkwardness, the <strong>Get-ADGroupMember<\/strong> cmdlet doesn&rsquo;t support using a Windows PowerShell or LDAP filter. Still, there&rsquo;s always a way when you have a Windows PowerShell pipeline to play with:<\/p>\n<p style=\"padding-left: 30px\">Get-ADGroup -Filter {Name -eq &#8216;English Scientists&#8217;} | Get-ADGroupMember\nFor this process, you don&rsquo;t care who is a member of the group because you are looking for empty groups or groups with a very low number of members. All you need is a count of the membership:<\/p>\n<p style=\"padding-left: 30px\">(Get-ADGroup -Filter {Name -eq &#8216;English Scientists&#8217;} | Get-ADGroupMember).Count\n-Or-<\/p>\n<p style=\"padding-left: 30px\">Get-ADGroup -Filter {Name -eq &#8216;English Scientists&#8217;} | Get-ADGroupMember |<\/p>\n<p style=\"padding-left: 30px\">Measure-Object | select -ExpandProperty Count\nThe first option is less typing; but unfortunately, the techniques breaks if there is only one group member, so this is better:<\/p>\n<p style=\"padding-left: 30px\">Import-Module ActiveDirectory<\/p>\n<p style=\"padding-left: 30px\">&nbsp;<\/p>\n<p style=\"padding-left: 30px\">Get-ADGroup -Filter * |<\/p>\n<p style=\"padding-left: 30px\">foreach {<\/p>\n<p style=\"padding-left: 30px\">&nbsp;New-Object -TypeName psobject -Property @{<\/p>\n<p style=\"padding-left: 30px\">&nbsp;GroupName = $_.Name<\/p>\n<p style=\"padding-left: 30px\">&nbsp;MemberCount = Get-ADGroupMember -Identity &#8220;$($_.samAccountName)&#8221; | Measure-Object | select -ExpandProperty Count<\/p>\n<p style=\"padding-left: 30px\">}<\/p>\n<p style=\"padding-left: 30px\">} | sort MemberCount&nbsp;\nYou actually do know the <strong>SamAccountName <\/strong>of the group because you are using <strong>Get-ADGroup<\/strong> to find the groups, which simplifies the script. One drawback to using <strong>New-Object<\/strong> is that the properties don&rsquo;t come out in the order you specify. With Windows PowerShell 4.0 or Windows PowerShell 3.0, you can use an ordered hash table to resolve this issue:<\/p>\n<p style=\"padding-left: 30px\">Import-Module ActiveDirectory<\/p>\n<p style=\"padding-left: 30px\">&nbsp;<\/p>\n<p style=\"padding-left: 30px\">Get-ADGroup -Filter * |<\/p>\n<p style=\"padding-left: 30px\">foreach {<\/p>\n<p style=\"padding-left: 30px\">&nbsp;$props = [ordered] @{<\/p>\n<p style=\"padding-left: 30px\">&nbsp;GroupName = $_.Name<\/p>\n<p style=\"padding-left: 30px\">&nbsp;MemberCount = Get-ADGroupMember -Identity &#8220;$($_.samAccountName)&#8221; | Measure-Object | select -ExpandProperty Count<\/p>\n<p style=\"padding-left: 30px\">&nbsp;}<\/p>\n<p style=\"padding-left: 30px\">&nbsp;New-Object -TypeName psobject -Property $props<\/p>\n<p style=\"padding-left: 30px\">} | sort MemberCount&nbsp;\nI would recommend changing the last line from:<\/p>\n<p style=\"padding-left: 30px\">&nbsp;| sort MemberCount\nto:<\/p>\n<p style=\"padding-left: 30px\">| sort MemberCount&nbsp; | Export-Csv&nbsp; -Path membercount.csv&nbsp; -NoTypeInformation&nbsp;&nbsp;&nbsp;\nThe data is available for future analysis, and you can always rerun the script to compare against your last listing.<\/p>\n<p style=\"padding-left: 30px\"><strong>Note <\/strong>&nbsp;One important point to remember is that some of the default groups that Active Directory creates, such as Cryptographic Operators, may well be empty. Don&rsquo;t delete those groups!\nThis is all very nice for those admins who are able to use the Windows PowerShell cmdlets.&nbsp; What about those people still using older versions of Windows Server on their domain controllers?\nLife gets a little bit more involved if you can&rsquo;t use the cmdlets. You have to fall back on scripting against the ADSI scripting interface in Active Directory. This involves using two .NET classes:<\/p>\n<ul>\n<li>System.DirectoryServices.DirectoryEntry<\/li>\n<li>System.DirectoryServices. DirectorySearcher<\/li>\n<\/ul>\n<p>That can be a lot to type, so the Windows PowerShell team very kindly gave us a couple of shortcuts.&nbsp; Instead of typing [System.DirectoryServices.DirectoryEntry], you just need to use <strong>[adsi]<\/strong>, and instead of [System.DirectoryServices.DirectorySearcher], you can use <strong>[adsisearcher]<\/strong>.\nIf you are still using Windows PowerShell&nbsp;1.0, <strong>[adsisearcher]<\/strong> is not available, so you&rsquo;ll need to use the full class name:<\/p>\n<p style=\"padding-left: 30px\">$root = [adsi]&#8221;&#8221;<\/p>\n<p style=\"padding-left: 30px\">$search = [adsisearcher]$root<\/p>\n<p style=\"padding-left: 30px\">$search.Filter = &#8220;(objectclass=group)&#8221;<\/p>\n<p style=\"padding-left: 30px\">$search.SizeLimit = 3000<\/p>\n<p style=\"padding-left: 30px\">$search.FindAll() |<\/p>\n<p style=\"padding-left: 30px\">foreach {<\/p>\n<p style=\"padding-left: 30px\">&nbsp;$group = $_.GetDirectoryEntry()&nbsp;<\/p>\n<p style=\"padding-left: 30px\">&nbsp;$count = ($group.Member).Count<\/p>\n<p style=\"padding-left: 30px\">&nbsp;if ($count -eq $null){$count = 0}<\/p>\n<p style=\"padding-left: 30px\">&nbsp;<\/p>\n<p style=\"padding-left: 30px\">&nbsp;$props = [ordered] @{<\/p>\n<p style=\"padding-left: 30px\">&nbsp;GroupName = $($group.Name)<\/p>\n<p style=\"padding-left: 30px\">&nbsp;MemberCount = $count<\/p>\n<p style=\"padding-left: 30px\">&nbsp;}<\/p>\n<p style=\"padding-left: 30px\">&nbsp;<\/p>\n<p style=\"padding-left: 30px\">&nbsp;New-Object -TypeName PSObject -Property $props<\/p>\n<p style=\"padding-left: 30px\">} | sort MemberCount&nbsp;\nThe script starts by getting a pointer to the root of the Active Directory domain. A <strong>DirectorySearcher<\/strong> object is created that uses the root as the base of its search. The search filter is set to find everything that matches <strong>&#8220;(objectclass=group)&#8221;<\/strong>&mdash;that is all groups. A limit of 3000 is set for the number of objects to return. You can modify this to match your environment (the default is 1000).\nThe <strong>FindAll()<\/strong> method is used to pull all of the groups in the domain. The objects that are returned from the search are not full Active Directory objects, so you need to iterate through them and use the <strong>GetDirectoryEntry()<\/strong> method to retrieve the group object.\nYou can then get a count of the members. In this technique, an empty group returns a NULL result. Convert that to zero&mdash;it is much easier to read and work with. The same previous hash table structure is used to create the output object, and a final sort displays the data with the empty groups at the top of the list.\nIf you compare the results, you will see some groups, such as Domain Users, in the output from the cmdlets that don&rsquo;t appear in the output from the script. This is because they are groups that are automatically maintained by Active Directory, and the ADSI interface ignores them.\nIf you want to extend the use of this script, you could:<\/p>\n<ul>\n<li>Add the distinguished name to the output.<\/li>\n<li>Create a script to compare the current list with a historical list to track changes.<\/li>\n<\/ul>\n<p>TL, that&rsquo;s how you use Windows PowerShell to audit your Active Directory for empty 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<\/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 <a href=\"http:\/\/bit.ly\/scriptingguystwitter\" target=\"_blank\">Twitter<\/a> and <a href=\"http:\/\/bit.ly\/scriptingguysfacebook\" target=\"_blank\">Facebook<\/a>. If you have any questions, send email to me at <a href=\"http:\/\/blogs.technet.commailto:scripter@microsoft.com\" target=\"_blank\">scripter@microsoft.com<\/a>, or post your questions on the <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 Active Directory groups that have no members. &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 audit our Active Directory to discover any empty groups. How can I do that? &mdash;TL &nbsp;Hello TL, 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":[7,453,51,44,56,189,3,4,45],"class_list":["post-2696","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-scripting","tag-active-directory","tag-admin-first-steps","tag-getting-started","tag-groups","tag-guest-blogger","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 Active Directory groups that have no members. &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 audit our Active Directory to discover any empty groups. How can I do that? &mdash;TL &nbsp;Hello TL, Honorary [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/2696","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=2696"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/posts\/2696\/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=2696"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/categories?post=2696"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/scripting\/wp-json\/wp\/v2\/tags?post=2696"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}