Hey, Scripting Guy! How Can I Use Windows PowerShell 2.0 to Find Active Directory Domain Services Groups Not Being Used?

Hey, Scripting Guy! How Can I Use Windows PowerShell 2.0 to Find Active Directory Domain Services Groups Not Being Used?

  • Comments 8
  • Likes

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to use Windows PowerShell 2.0 to find groups in Active Directory Domain Services (AD DS) that are not being used. Over the years, we have had several network administrators. Some of them were good, and some had challenges in the “good” category. As a result, there are groups that do not have any members. I would like to be able to run a Windows PowerShell script that would go through all the groups in the domain and delete the ones that are not being used. Is this something that can be accomplished?

-- EM

 

Hey, Scripting Guy! Answer

Hello EM,

Microsoft Scripting Guy Ed Wilson here. Have I mentioned recently how much I love working with Windows PowerShell? The cool thing about Windows PowerShell is that a script is never finished. There is always something else that can be done, and the Get-UnusedGroups.ps1 script that I wrote for you is no exception. It started out as a simple “one liner” and it kind of grew from there. I used the Get-MyModule module to ensure the script has access to the Active Directory cmdlets. I then decided to add a function to create bogus empty groups in Active Directory to ensure you had groups you could use for testing purposes. Later, I decided you might want to control where the bogus groups are created. Then I added the Get-UnusedGroups function.

As I was working with it, I thought it might be a good idea to exclude critical system objects from the retrieval process. But, what if Microsoft Exchange is installed? Unfortunately, Microsoft Exchange does not mark its groups as critical system objects, but it does place them in a default organizational unit. So I added that OU to the exclusion list. Later I found there are a couple of DNS groups that exist that should be excluded from the search, and that completed the Get-UnusedGroups function. I then decided that producing nice formatted output would be worthwhile, so I created the Format-Output function to create a nice table. Lastly, I created the Remove-UnusedGroups function to remove the unused groups that might be discovered. I also added some command-line switches and other parameters to allow you to create bogus groups or not, and to allow you to remove unused groups or not. The report is always created. The complete Get-UnusedGroups.ps1 script is shown here.

Get-UnusedGroups.ps1

<#
.Synopsis
Queries Active Directory for groups that are unused. It will remove
unused groups, or simply report. In addition, it can create empty
groups for test purposes 
.Example
Get-UnusedGroups.ps1 -CreateGroups -path "ou=testou,dc=nwtraders,dc=com" `
-numberGroups 5
Creates 5 empty global groups in the TestOU of nwtraders.com. Groups will 
be named test1, test2 ... test5 
.Example
Get-UnusedGroups.ps1 -CreateGroups -path "ou=testou,dc=nwtraders,dc=com" `
-numberGroups 5 -SearchBase "ou=testou,dc=nwtraders,dc=com"
Creates 5 empty global groups in the TestOU of nwtraders.com. Groups will 
be named test1, test2 ... test5. It then searches the testou in nwtraders.com
for empty groups and produces a report.
.Example
Get-UnusedGroups.ps1 -searchBase "ou=testou,dc=nwtraders,dc=com" -remove
Searches the testou in nwtraders.com for groups with no members. If these
are found they will be removed. A report is produced of groups found, but 
no bogus groups are created. 
.Parameter numberGroups
The number of groups to be created if the -createGroups switch is used
.Parameter CreateGroups
Causes script to create groups. Must be used with numberGroups and path.
.Parameter path
Location of bogus groups to be created. Must be used with numberGroups
and CreateGroups.
.Parameter SearchBase
Location to search for empty groups.
.Parameter remove
Causes script to remove empty groups. Uses SearchBase parameter.
.Inputs
[psobject]
.Outputs
[psobject]
.Notes
NAME: Get-UnusedGroups.ps1
AUTHOR: Ed Wilson
AUTHOR BOOK: Windows PowerShell 2.0 Best Practices, Microsoft Press 2010
LASTEDIT: 7/19/2010
HSG: hsg-07-22-10
KEYWORDS: Active Directory, Groups
.Link
Http://www.ScriptingGuys.com
Http://www.bit.ly/HSGBlog
#>
#Requires -Version 2.0
Param(
[int16]$numberGroups,
[switch]$CreateGroups,
[string]$path, 
[string]$searchBase,
[switch]$remove
) #end param
Function Get-MyModule
{
Param([string]$name)
if(-not(Get-Module -name $name)) 
{ 
if(Get-Module -ListAvailable | 
Where-Object { $_.name -eq $name })
{ 
Import-Module -Name $name 
$true
} #end if module available then import
else { $false } #module not available
} # end if not module
else { $true } #module already loaded
} #end function get-MyModule 
Function New-BogusTestGroups
{
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[int16]$numberGroups,
[Parameter(Mandatory = $True)]
[string]$path
)
1..$numberGroups | 
ForEach-Object { New-ADGroup -name "test$_" -groupScope global -path $path}
$numberGroups = $path = $null
} #end function New-BogusTestGroups
Function Get-UnusedGroups
{
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[string]$searchBase
)
Get-ADGroup -Filter * -Properties members, isCriticalSystemObject `
-SearchBase $searchBase | 
Where-Object { ($_.members.count -eq 0 -AND !($_.IsCriticalSystemObject) `
-AND $_.DistinguishedName -notMatch 'Exchange Security' -AND `
$_.DistinguishedName -notMatch 'Dns') }
$searchBase = $null
} #end function Get-UnusedGroups
Function Remove-UnusedGroups
{
$input |
Remove-ADGroup 
} #end function remove-unusedGroups
Function Format-Output
{ $input |
Sort-Object -Property groupscope | 
Format-Table -Property groupscope, name, distinguishedName -AutoSize -Wrap
} # end function format-output
# *** Entry point to script ***
If(-not (Get-MyModule -name "ActiveDirectory")) { exit }
if($CreateGroups) {New-BogusTestGroups -numberGroups $numberGroups -path $path}
If($searchBase) {Get-UnusedGroups -SearchBase $searchBase | Format-Output}
if($remove) { Get-UnusedGroups -SearchBase $searchBase | Remove-UnusedGroups }

The New-BogusTestGroups function is used to create a number of global groups that do not have any members assigned to them. This function can be used to provide a control to your script to ensure it is picking up groups that do not have any members in it. You can use the script simply to create bogus groups to use for other purposes as well. There is no requirement to delete the empty groups when the script has finished running. You will need to supply two values to the New-BogusTestGroups function. The first is the -numberGroups parameter. This value is used to control how many empty groups will be created. The second parameter is the path to the location that will hold the newly created groups. Normally this would be the distinguishedName attribute of an organizational unit.

The New-BogusTestGroups pipes an array of numbers to the ForEach-Object cmdlet. Inside the scriptblock for the Foreach-Object cmdlet, the New-ADGroup cmdlet is used to create the new groups. The name of the group will be test and the number from the piped array. Because both the numberGroups and the path parameters are marked as mandatory, an error will be displayed if the values are not supplied. The New-BogusTestGroups function is shown here:

 

Function New-BogusTestGroups

{

[CmdletBinding()]

Param(

[Parameter(Mandatory = $True)]

[int16]$numberGroups,

[Parameter(Mandatory = $True)]

[string]$path

)

1..$numberGroups | 

ForEach-Object { New-ADGroup -name "test$_" -groupScope global -path $path}

$numberGroups = $path = $null

} #end function New-BogusTestGroups

When the script is run, and only the numberGroups, CreateGroups, and Path parameters are used, no output is displayed. This is shown in the following image.

Image of no output displayed

When the destination is checked in the Active Directory Users and Computers MMC, the test groups appear. This is shown in the following image.

Image of test groups

On the other hand, if one of the required parameters is not supplied, an error will be generated. The script is smart enough to tell you which parameter is missing. In the following image, it is the path parameter that was not supplied.

Image of missing path parameter identified

The Get-UnusedGroups function accepts a single parameter, the SearchBase. It uses the Get-ADGroup cmdlet to search for all groups. The members property is not returned by default, nor is the isCriticalSystemObject property; therefore, these properties are requested in the command. The Where-Object cmdlet is used to filter out the resulting groups. The Where-Object cmdlet looks for groups that do not have any members and are not critical system objects. In addition, it skips the exchange security groups and any group that has the letters DNS in the distinguishedName. The Get-UnusedGroups function is only called if the SearchBase parameter is supplied:

 

Function Get-UnusedGroups
{
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[string]$searchBase
)
Get-ADGroup -Filter * -Properties members, isCriticalSystemObject `
-SearchBase $searchBase | 
Where-Object { ($_.members.count -eq 0 -AND !($_.IsCriticalSystemObject) `
-AND $_.DistinguishedName -notMatch 'Exchange Security' -AND `
$_.DistinguishedName -notMatch 'Dns') }
$searchBase = $null
} #end function Get-UnusedGroups

When the Get-UnusedGroups function is called, the Format-Output function is automatically called. This is not due to the way the Get-UnusedGroups function is written, but due to the way the function is called in the script. This is shown here:

 

If($searchBase) {Get-UnusedGroups -SearchBase $searchBase | Format-Output}

The Format-Output function uses the $input automatic variable to accept the pipeline results from the Get-UnusedGroups function. The list of groups is first sorted by groupscope, and then a table is produced. The Format-Output function is shown here:

 

Function Format-Output
{ $input |
Sort-Object -Property groupscope | 
Format-Table -Property groupscope, name, distinguishedName -AutoSize -Wrap
} # end function format-output

The resulting output from the Format-Output function is seen in the following image.

Image of output from Format-Output function

When the Remove-UnusedGroups function is called, it accepts the input from the Get-UnusedGroups function and pipes the groups to the Remove-ADGroup cmdlet. Unfortunately, the Remove-ADGroup cmdlet does not have a –force switch, so it will prompt you when you wish to delete a group. This is shown in the following image. Fortunately, you can tell it to remove all, but this behavior prevents an unattended operation.

 

Function Remove-UnusedGroups
{
$input |
Remove-ADGroup 
} #end function remove-unusedGroups

Image of yes/no prompt

 

EM, that is all there is to using Active Directory cmdlets to delete unused groups in Active Directory. This also concludes Active Directory Week. Join us tomorrow for Quick-Hits Friday.

We would love for you to follow us on Twitter and Facebook. If you have any questions, send email to us at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • I'm trying to clean up AD, the distributions list right not, do you have a script that will tell when a distribution list was last used and by who?

  • That's a cool code.

    But doesn't work on domains that do not run win 2008 r2.

    here is my function:

    http://poshcode.org/2023

  • That code seems cool for sure but what about this one liner ;-) ?

    adfind -f "&(objectclass=group)(!member=*)" -dsq|admod -unsafe -rm

    Joe rules !!

  • ...one attribute that contains a date/time stamp which shows the "last time a group was queried", and one attribute which shows "what queried that group", this would help significantly in removing old groups from large AD deployments that aren't well documented...please please please get the Directory Services team to consider :)

  • Hi Guys,

    I am really a Fanatic of powershell, and also thank you for the additional knowledge that you posted here and its very useful..but one thing that i did not find in any site on how to remove global security group in an AD user using powershell v2. someone can help me please :)..i really need it in my daily task as an IT systems.

    thank you in advance

    Gerry

  • This Powershell command will give me a listing of all the security groups in my AD:

    Get-ADGroup -filter {GroupCategory -eq "Security"}

    How do I modify the command to give me a list of all groups created or changed before a certain date?  I've opened the properties of the group in ADSIEDIT.msc and I can see these two fields:

    whenChanged

    whenCreated

  • Before you just delete the "empty groups", make sure you're checking for primaryGroupID (e.g. 'Domain Users')

  • This is good if no one is in the groups not a real world example though, The real question would be how to tell if the group memebership has been not used. Expiring the groups is not really a good way to accomplish this. More administrative effort if you have thousands of distro /user/ computer groups.