<# This script will enumerate all group objects in a Domain, providing both a high level overview and a full report based on the values of the following attributes: - name - distinguishedname - samaccountname - mail - grouptype - displayname - description - member - memberof - info - isCriticalSystemObject - admincount - managedBy - expirationtime (requires Exchange Schema Extensions) - whencreated - whenchanged - sidhistory - objectsid - proxyAddresses (requires Exchange Schema Extensions) - legacyExchangeDN (requires Exchange Schema Extensions) - mailNickName (requires Exchange Schema Extensions) - reportToOriginator (requires Exchange Schema Extensions) - gidNumber - msSFU30Name - msSFU30NisDomain We further derive: - The Parent OU from the distinguishedname attribute. - The GroupCategory and GroupScope from the grouptype attribute. - The MemberCount from the member attribute. - MailEnabled from proxyAddresses, legacyExchangeDN, mailNickName, and reportToOriginator. reportToOriginator must be set to TRUE. This is not well documented. - Expired from the expirationtime attribute. - Conflict from the name and samaccountname attributes. - UnixEnabled from gidnumber, mssfu30name, mssfu30nisdomain. - Exclude: When reviewing groups it's important to flag the ones that are marked as Critical System Objects where their isCriticalSystemObject attribute is set to True, Protected Objects (AdminSDHolder) where their adminCount attribute is set to 1, and various other important groups that should not be removed. To be able to capture the "other" groups we place them into the $ExclusionGroups and $ExclusionOUs arrays. Now, you may have groups marked by the AdminSDHolder that no longer require protection. It happens due to group nesting, and is not something that's automatically removed when the group falls out of scope. Therefore it's still marked as protected. This is often unintentional and typically misunderstood. You'll need to review each one to unblock inheritance and clear the adminCount attribute where they fall out of scope. However, I recommend using a script written by Tony Murray to clean up the AdminSDHolder: http://www.open-a-socket.com/index.php/2013/09/11/cleaning-up-adminsdholder-orphans/ Further information about the AdminSDHolder can be found here: - http://social.technet.microsoft.com/wiki/contents/articles/22331.adminsdholder-protected-groups-and-security-descriptor-propagator.aspx - http://technet.microsoft.com/en-us/magazine/2009.09.sdadminholder.aspx - http://www.selfadsi.org/extended-ad/ad-permissions-adminsdholder.htm - http://blogs.technet.com/b/askds/archive/2009/05/07/five-common-questions-about-adminsdholder-and-sdprop.aspx I have seen situations where the adminCount attribute is set to 5. Whilst this is an invalid and undocumented value the adminCount attribute is 4 bytes (32 bits) in size. Valid values are 0 (disabled), 1 (enabled), or not set (disabled - default). So it's simply enabled or disabled based on the least significant bit. Converting 5 into binary, it's least significant bit is 1. Therefore, we take a setting of 5 to mean that it's enabled. We group together the following 4 attributes to determine if a group is mail-enabled: proxyAddresses, legacyExchangeDN, mailNickName, and reportToOriginator: - http://pmoreland.blogspot.com.au/2013/03/creating-mail-contacts-and-distribution.html Groups whose name contains CNF: and/or sAMAccountName contains $Duplicate means that it's a duplicate account caused by conflicting/duplicate objects. This typically occurs when objects are created on different Read Write Domain Controllers at nearly the same time. After replication kicks in and those conflicting/duplicate objects replicate to other Read Write Domain Controllers, Active Directory replication applies a conflict resolution mechanism to ensure every object is and remains unique. You can't just delete the conflicting/ duplicate objects, as these may often be in use. You need to merge the group membership and ensure the valid group is correctly applied to the resource. Then you can confidently delete the conflicting/duplicate group. In environments where the Exchange Schema has been deployed a nice way to manage groups it to set their expirationTime attribute. This will give us the ability to implement a nice lifecycle management process. You can go one step further and add a user or mail enabled security group to the managedBy attribute. This will give us the ability to implement some workflow when the group is x days before expiring. Syntax examples: - To execute the script in the current Domain: Get-GroupReport.ps1 - To execute the script against a trusted Domain: Get-GroupReport.ps1 -TrustedDomain mydemosthatrock.com Script Name: Get-GroupReport.ps1 Release 1.9 Written by Jeremy@jhouseconsulting.com 16/12/2014 Modified by Jeremy@jhouseconsulting.com 06/05/2015 #> #------------------------------------------------------------- param([String]$TrustedDomain,[switch]$verbose) Set-StrictMode -Version 2.0 if ($verbose.IsPresent) { $VerbosePreference = 'Continue' Write-Verbose "Verbose Mode Enabled" } Else { $VerbosePreference = 'SilentlyContinue' } #------------------------------------------------------------- # Set this to the OU structure where the you want to search to # start from. Do not add the Domain DN. If you leave it blank, # the script will start from the root of the domain. $OUStructureToProcess = "" # Set the name of the attribute you want to populate for objects # to be evaluated as a stale or non-stale object. $ExcludeAttribute = "comment" # Set the text within the $ExcludeAttribute that you want to use # to evaluate if the object should be excluded from the stale # object collection. $ExcludeText = "Decommission=False" # Set this to the delimiter for the CSV output $Delimiter = "," # Set this to remove the double quotes from each value within the # CSV. $RemoveQuotesFromCSV = $False # Set this value to true if you want to see the progress bar. $ProgressBar = $True # Although some of the default groups are not marked as a Critical # System Objects or Protected Objects (AdminSDHolder) they must # still be excluded from deletion as a good practice. # http://technet.microsoft.com/en-us/library/dn579255.aspx # On top of this we exclude the RTC groups as part of OCS/Lync and # also the "Microsoft Exchange" OUs. $ExclusionGroups = @( "DnsAdmins",` "DnsUpdateProxy",` "DHCP Users",` "DHCP Administrators",` "Offer Remote Assistance Helpers",` "TelnetClients",` "IIS_WPG",` "Access Control Assistance Operators",` "Cloneable Domain Controllers",` "Hyper-V Administrators",` "Protected Users",` "RDS Endpoint Servers",` "RDS Management Servers",` "RDS Remote Access Servers",` "Remote Management Users",` "WinRMRemoteWMIUsers_",` "RTC*" ) $ExclusionOUs = @( "*Microsoft Exchange System Objects*" "*Microsoft Exchange Security Groups*" ) #------------------------------------------------------------- $invalidChars = [io.path]::GetInvalidFileNamechars() $datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-") # Get the script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ReferenceFileFull = $(&$ScriptPath) + "\GroupReport-Full-$($datestampforfilename).csv" $ReferenceFileSummary = $(&$ScriptPath) + "\GroupReport-Summary-$($datestampforfilename).csv" $ReferenceFileSummaryTotals = $(&$ScriptPath) + "\GroupReport-Summary-Totals-$($datestampforfilename).csv" if (Test-Path -path $ReferenceFileFull) { remove-item $ReferenceFileFull -force -confirm:$false } if (Test-Path -path $ReferenceFileSummary) { remove-item $ReferenceFileSummary -force -confirm:$false } if (Test-Path -path $ReferenceFileSummaryTotals) { remove-item $ReferenceFileSummaryTotals -force -confirm:$false } Function CheckIfExchangeSchemaIsPresent { Param($root) # This function check to see if the ms-Exch-Schema-Verison-PT attribute is present. This # test is used to determine if the Exchange Schema is present. If ($root -eq "CurrentForest" -OR $root -eq "" -OR $root -eq $NULL) { $RootDSE = [ADSI]"LDAP://RootDSE" } Else { $RootDSE = [ADSI]"LDAP://$Root/RootDSE" } $version = "CN=ms-Exch-Schema-Version-Pt,$($RootDSE.schemaNamingContext)" $value = [ADSI]::Exists( "LDAP://$version" ) return $value } if ([String]::IsNullOrEmpty($TrustedDomain)) { # Get the Current Domain Information $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() } else { $context = new-object System.DirectoryServices.ActiveDirectory.DirectoryContext("domain",$TrustedDomain) Try { $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($context) } Catch [exception] { write-host -ForegroundColor red $_.Exception.Message Exit } } # Get AD Distinguished Name $DomainDistinguishedName = $Domain.GetDirectoryEntry() | select -ExpandProperty DistinguishedName If ($OUStructureToProcess -eq "") { $ADSearchBase = $DomainDistinguishedName } else { $ADSearchBase = $OUStructureToProcess + "," + $DomainDistinguishedName } # Check if the Exchange Schema is present by calling the CheckIfExchangeSchemaIsPresent function if ([String]::IsNullOrEmpty($TrustedDomain)) { $IsExchangePresent = CheckIfExchangeSchemaIsPresent "CurrentForest" } else { $IsExchangePresent = CheckIfExchangeSchemaIsPresent $TrustedDomain } $TotalGroupsProcessed = 0 $GroupCount = 0 $GlobalDistributionGroups = 0 $DomainLocalDistributionGroups = 0 $UniversalDistributionGroups = 0 $GlobalSecurityGroups = 0 $DomainLocalSecurityGroups = 0 $BuiltinLocalSecurityGroups = 0 $UniversalSecurityGroups = 0 $UnrecognisedGroupTypes = 0 $GroupsHashTable = @{} $TotalNoMembers = 0 $TotalMailEnabledObjects = 0 $TotalMailEnabledDistributionGroups = 0 $TotalCriticalSystemObjects = 0 $TotalProtectedObjects = 0 $TotalExcludedObjects = 0 $TotalToSubtract = 0 $TotalExpiredObjects = 0 $TotalConflictingObjects = 0 $TotalWithSIDHistory = 0 $TotalUnixEnabledObjects = 0 $TotalNoManagedBy = 0 # Create an LDAP search for all groups $ADFilter = "(objectClass=group)" # There is a known bug in PowerShell requiring the DirectorySearcher # properties to be in lower case for reliability. $ADPropertyList = @("name","distinguishedname","samaccountname","mail","grouptype", ` "displayname","description","member","memberof","info", ` "isCriticalSystemObject","admincount","managedBy","objectsid", ` "expirationtime","whencreated","whenchanged","sidhistory", ` "proxyaddresses","legacyexchangedn","mailnickname", ` "reporttooriginator","gidnumber","mssfu30name","mssfu30nisdomain") $ADScope = "SUBTREE" $ADPageSize = 1000 $ADSearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($ADSearchBase)") $ADSearcher = New-Object System.DirectoryServices.DirectorySearcher $ADSearcher.SearchRoot = $ADSearchRoot $ADSearcher.PageSize = $ADPageSize $ADSearcher.Filter = $ADFilter $ADSearcher.SearchScope = $ADScope if ($ADPropertyList) { foreach ($ADProperty in $ADPropertyList) { [Void]$ADSearcher.PropertiesToLoad.Add($ADProperty) } } Try { write-host -ForegroundColor Green "`nPlease be patient whilst the script retrieves all group objects and specified attributes..." $colResults = $ADSearcher.Findall() # Dispose of the search and results properly to avoid a memory leak $ADSearcher.Dispose() $GroupCount = $colResults.Count } Catch { $GroupCount = 0 Write-Host -ForegroundColor red "The $ADSearchBase structure cannot be found!" } if ($GroupCount -ne 0) { write-host -ForegroundColor Green "`nProcessing $GroupCount group objects in the $domain Domain..." $colResults | ForEach-Object { $Name = $_.Properties.name[0] $GroupDN = $_.Properties.distinguishedname[0] $ParentOU = $GroupDN -split '(?