{"id":3171,"date":"2019-05-31T23:28:17","date_gmt":"2019-05-31T23:28:17","guid":{"rendered":"https:\/\/www.wapshere.com\/missmiis\/?page_id=3171"},"modified":"2019-05-31T23:36:48","modified_gmt":"2019-05-31T23:36:48","slug":"compare-adgroups-ps1","status":"publish","type":"page","link":"https:\/\/www.wapshere.com\/missmiis\/compare-adgroups-ps1","title":{"rendered":"Compare-ADGroups.ps1"},"content":{"rendered":"<pre>\r\nPARAM(\r\n    $SearchBaseA,\r\n    $SearchScopeA=\"Subtree\",\r\n    $SearchBaseB,\r\n    $SearchScopeB=\"Subtree\",\r\n    $CSVGroupsB,\r\n    $Delimiter=\"`t\",\r\n    $MinMember = 5,\r\n    $CountPercentThreshold = 75,\r\n    $ReportThreshold = 50,\r\n    $ReportFile = \".\\GroupMembershipComparison.csv\"\r\n)\r\n<#-----------------------------------------------------------------------------\r\nGroup report &#038; Search for groups with duplicate group membership\r\n\r\nBased on an original script from:\r\n    Ashley McGlone - GoateePFE\r\n    Microsoft Premier Field Engineer\r\n    http:\/\/aka.ms\/GoateePFE\r\n    January, 2014\r\n\r\nUpdated to help with RBAC group analysis by:\r\n    Carol Wapshere MVP (MIM \/ Enterprise Mobility)\r\n    www.wapshere.com\r\n    January 2016\r\n\r\n-------------------------------------------------------------------------------\r\nLEGAL DISCLAIMER\r\nThis Sample Code is provided for the purpose of illustration only and is not\r\nintended to be used in a production environment.  THIS SAMPLE CODE AND ANY\r\nRELATED INFORMATION ARE PROVIDED \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER\r\nEXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF\r\nMERCHANTABILITY AND\/OR FITNESS FOR A PARTICULAR PURPOSE.  We grant You a\r\nnonexclusive, royalty-free right to use and modify the Sample Code and to\r\nreproduce and distribute the object code form of the Sample Code, provided\r\nthat You agree: (i) to not use Our name, logo, or trademarks to market Your\r\nsoftware product in which the Sample Code is embedded; (ii) to include a valid\r\ncopyright notice on Your software product in which the Sample Code is embedded;\r\nand (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and\r\nagainst any claims or lawsuits, including attorneys\u00e2\u20ac\u2122 fees, that arise or result\r\nfrom the use or distribution of the Sample Code.\r\n-------------------------------------------------------------------------------\r\n\r\nThis script has been modified from the original which compares group memberships\r\nin an entire domain to find membership duplication. This script adds the following \r\nextra functions:\r\n- Compare groups in one OU\/entire domain to groups in another OU, OR\r\n- Compare groups in one OU\/entire domain to a CSV of proposed groups.\r\n\r\nPARAMETERS:\r\n-SearchBaseA    (Optional) Select the \"A\" list of groups from this OU only,\r\n-SearchScopeA   (Optional) Set to either \"OneLevel\" or \"Subtree\" for use with SearchBaseA.\r\n-SearchBaseB    (Optional) Select the \"B\" list of groups from this OU only,\r\n-SearchScopeB   (Optional) Set to either \"OneLevel\" or \"Subtree\" for use with SearchBaseB.\r\n-CSVGroupsB     (Optional) Use instead of SearchBaseB to define the group \"B\" list, in this case\r\n                           they are proposed rather than actual groups. The column headers must be\r\n                           \"Name\" and \"DN\", where the DN column contains the DN of expected members.\r\n-Delimiter      (Optional) Set a different delimiter for CSVGroupsB. Default is \"`t\" tab character.\r\n-MinMember      (Required) Minimum size of group to compare. Must be at least 1.\r\n-CountPercentThreshold   (Required) Only compare groups with a membership within this percent\r\n                         size of each other. Set to 0 to disable this check. This is useful if\r\n                         looking for possible role-based groups to nest in a larger group without\r\n                         necessarily matching the entire membership.\r\n\r\n-------------------------------------------------------------------------------\r\nOriginal script notes:\r\n\r\nComparing all groups in AD involves an \"n * n-1\" number of comparisons. The following\r\nsteps have been taken to make the comparisons more efficient:\r\n\r\n- Minimum number of members in a group before it is considered for matching\r\n  This automatically filters out empty groups and those with only a few members.\r\n  This is an arbitrary number. Default is 5. Must be at least 1.\r\n\r\n- Minimum percentage of overlap between group membership counts to compare\r\n  ie. It only makes sense to compare groups whose total membership are close\r\n  in number. You wouldn't compare a group with 5 members to a group with 65\r\n  members when seeking a high number of group member duplicates. By default\r\n  the lowest group count must be within 25% of the highest group count.\r\n\r\n- Does not compare the group to itself.\r\n\r\n- The pair of groups has not already been compared.\r\n\r\nGroups of all types are compared against each other in order to give a complete\r\npicture of group duplication (Domain Local, Global, Universal, Security,\r\nDistribution). If desired, mismatched group category and scope can be filtered\r\nout in Excel when viewing the CSV file output.\r\n\r\nUsing the data from this report you can then go investigate groups for\r\nconsolidation based on high match percentages.\r\n\r\n-------------------------------------------------------------------------------\r\n\r\nThe group list report gives you handy fields for analyzing your groups for\r\ncleanup:  whenCreated, whenChanged, MemberCount, MemberOfCount, SID,\r\nSIDHistory, DaysSinceChange, etc.  Use these columns to filter or pivot in\r\nExcel for rich reports.  For example:\r\n- Groups with zero members\r\n- Groups unchanged in 1 year\r\n- Groups with SID history to cleanup\r\n- Etc.\r\n-------------------------------------------------------------------------sdg-#>\r\n\r\nImport-Module ActiveDirectory\r\n\r\n# Depending on whether we're comparing real or proposed groups we need to use a different\r\n# type of identifier to log the comparison as \"done\".\r\n$idA = \"SID\"\r\nif ($CSVGroupsB) {$idB = \"Name\"} else {$idB = \"SID\"}\r\n\r\n# If Search Bases not specified get from the current domain\r\n$MyDomain = (Get-ADDomain).DistinguishedName\r\nif (-not $SearchBaseA) {$SearchBaseA = $MyDomain}\r\nif (-not $SearchBaseB) {$SearchBaseB = $MyDomain}\r\n\r\n\r\n#region########################################################################\r\n# List of all groups and the count of their member\/memberOf\r\n# You could edit this query to limit the scope and filter by:\r\n# - group name pattern\r\n#      -Filter {name -like \"*foo*\"}\r\n# - group scope\r\n#      -Filter {GroupScope -eq 'Global'}\r\n# - group category\r\n#      -Filter {GroupCategory -eq 'Security'}\r\n# - OU path\r\n#      -SearchBase 'OU=Groups,OU=NA,DC=contoso,DC=com' -SearchScope SubTree\r\n# - target GC port 3268 and query for only Universal groups to compare\r\n#      -Server DC1.contoso.com:3268 -Filter {GroupScope -eq \"Universal\"}\r\n# - etc.\r\nWrite-Progress -Activity \"Getting group A list...\" -Status \"...\"\r\n$GroupListA = Get-ADGroup -Filter * -SearchBase $SearchBaseA -SearchScope $SearchScopeA `\r\n        -Properties Name, DistinguishedName, `\r\n        GroupCategory, GroupScope, whenCreated, whenChanged, member, `\r\n        memberOf, sIDHistory, SamAccountName, Description |\r\n    Select-Object Name, DistinguishedName, GroupCategory, GroupScope, `\r\n        whenCreated, whenChanged, member, memberOf, SID, SamAccountName, `\r\n        Description, `\r\n        @{name='MemberCount';expression={$_.member.count}}, `\r\n        @{name='MemberOfCount';expression={$_.memberOf.count}}, `\r\n        @{name='SIDHistory';expression={$_.sIDHistory -join ','}}, `\r\n        @{name='DaysSinceChange';expression=`\r\n            {[math]::Round((New-TimeSpan $_.whenChanged).TotalDays,0)}} |\r\n    Sort-Object Name\r\n\r\n$GroupListA |\r\n    Select-Object Name, SamAccountName, Description, DistinguishedName, `\r\n        GroupCategory, GroupScope, whenCreated, whenChanged, DaysSinceChange, `\r\n        MemberCount, MemberOfCount, SID, SIDHistory |\r\n    Export-CSV .\\GroupListA.csv -NoTypeInformation\r\n\r\n\r\nif ($CSVGroupsB)\r\n{\r\n    $GroupListB = import-csv $CSVGroupsB -Delimiter $Delimiter\r\n    # to do build array which includes member count\r\n}\r\nelseif ($SearchBaseA -eq $SearchBaseB -and $SearchScopeA -eq $SearchScopeB)\r\n{\r\n    $GroupListB = $GroupListA\r\n}\r\nelse\r\n{\r\n    Write-Progress -Activity \"Getting group B list...\" -Status \"...\"\r\n    $GroupListB = Get-ADGroup -Filter * -SearchBase $SearchBaseB -SearchScope $SearchScopeB `\r\n            -Properties Name, DistinguishedName, `\r\n            GroupCategory, GroupScope, whenCreated, whenChanged, member, `\r\n            memberOf, sIDHistory, SamAccountName, Description |\r\n        Select-Object Name, DistinguishedName, GroupCategory, GroupScope, `\r\n            whenCreated, whenChanged, member, memberOf, SID, SamAccountName, `\r\n            Description, `\r\n            @{name='MemberCount';expression={$_.member.count}}, `\r\n            @{name='MemberOfCount';expression={$_.memberOf.count}}, `\r\n            @{name='SIDHistory';expression={$_.sIDHistory -join ','}}, `\r\n            @{name='DaysSinceChange';expression=`\r\n                {[math]::Round((New-TimeSpan $_.whenChanged).TotalDays,0)}} |\r\n        Sort-Object Name\r\n\r\n    $GroupListB |\r\n        Select-Object Name, SamAccountName, Description, DistinguishedName, `\r\n            GroupCategory, GroupScope, whenCreated, whenChanged, DaysSinceChange, `\r\n            MemberCount, MemberOfCount, SID, SIDHistory |\r\n        Export-CSV .\\GroupListB.csv -NoTypeInformation\r\n\r\n}\r\n\r\n# Buid the list of comparisons to do\r\n$ToDo = @{}\r\n$i = 0\r\nforeach ($GroupA in ($GroupListA | Where-Object {$_.MemberCount -ge $MinMember}))\r\n{\r\n    $ToDo.Add($GroupA.($idA),@())\r\n    $CountA = $GroupA.MemberCount\r\n\r\n    foreach ($GroupB in ($GroupListB | Where-Object {$_.MemberCount -ge $MinMember}))\r\n    {\r\n        if ($GroupB.($idB) -ne $GroupA.($idA) `\r\n            -and -not $ToDo.ContainsKey($GroupB.($idB)))\r\n        {\r\n            $CountB = $GroupB.MemberCount\r\n\r\n            # Calculate the percentage size difference between the two groups\r\n            If ($CountA -le $CountB) {\r\n                $CountPercent = $CountA \/ $CountB * 100\r\n            } Else {\r\n                $CountPercent = $CountB \/ $CountA * 100\r\n            }\r\n\r\n            # If specified check the percentage difference in two group sizes is not more than $CountPercentThreshold\r\n            If ( ($CountPercentThreshold -eq 0) -or `\r\n             $CountPercent -ge $CountPercentThreshold ) \r\n            {\r\n                $ToDo.($GroupA.($idA)) += $GroupB.($idB)\r\n                $i += 1\r\n            }\r\n        }\r\n    }\r\n}\r\nwrite-host \"$i group comparisons will be made\"\r\n\r\n#endregion#####################################################################\r\n\r\n#region########################################################################\r\n\r\n# Start writing report file\r\n\r\n\"NameA,NameB,CountA,CountB,CountEqual,MatchPercentA,MatchPercentB,ScopeA,ScopeB,CategoryA,CategoryB,DNA,DNB\" | out-file $ReportFile -Encoding Default\r\n\r\n# Outer loop through A groups\r\n\r\n$progress = 0\r\nForEach ($a in $ToDo.Keys) \r\n{\r\n    $GroupA = $GroupListA | where {$_.($idA) -eq $a}\r\n    $CountA = $GroupA.MemberCount\r\n    $progress += 1\r\n\r\n    # Inner loop through B groups\r\n\r\n    ForEach ($b in $ToDo.($a)) \r\n    {\r\n        $GroupB = $GroupListB | where {$_.($idB) -eq $b}\r\n        $CountB = $GroupB.MemberCount\r\n\r\n        Write-Progress `\r\n            -Activity \"Comparing members of $($GroupA.Name)\" `\r\n            -Status \"To members of $($GroupB.Name)\" `\r\n            -PercentComplete ($progress\/$ToDo.Count * 100)\r\n        \r\n        \r\n        # This is the heart of the script. Compare group memberships.\r\n        $co = Compare-Object -IncludeEqual `\r\n            -ReferenceObject $GroupA.Member `\r\n            -DifferenceObject $GroupB.Member\r\n        $CountEqual = ($co | Where-Object {$_.SideIndicator -eq '=='} | `\r\n            Measure-Object).Count\r\n\r\n        $PercentMatchA = [math]::Round($CountEqual \/ $CountA * 100,2)\r\n        $PercentMatchB = [math]::Round($CountEqual \/ $CountB * 100,2)\r\n\r\n        # Add an entry for GroupA\/GroupB\r\n        if ($PercentMatchA -ge $ReportThreshold -or $PercentMatchB -ge $ReportThreshold)\r\n        {\r\n            $report = '\"' + $GroupA.Name + '\",\"' +\r\n                            $GroupB.Name + '\",\"' +\r\n                            $CountA + '\",\"' +\r\n                            $CountB + '\",\"' +\r\n                            $CountEqual + '\",\"' +\r\n                            $PercentMatchA + '\",\"' +\r\n                            $PercentMatchB + '\",\"' +\r\n                            $GroupA.GroupScope + '\",\"' +\r\n                            $GroupB.GroupScope + '\",\"' +\r\n                            $GroupA.GroupCategory + '\",\"' +\r\n                            $GroupB.GroupCategory + '\",\"' +\r\n                            $GroupA.DistinguishedName + '\",\"' +\r\n                            $GroupB.DistinguishedName\r\n            $report | Add-Content $ReportFile\r\n        }\r\n    }\r\n} \r\n\r\n#endregion#####################################################################\r\n\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>PARAM( $SearchBaseA, $SearchScopeA=&#8221;Subtree&#8221;, $SearchBaseB, $SearchScopeB=&#8221;Subtree&#8221;, $CSVGroupsB, $Delimiter=&#8221;`t&#8221;, $MinMember = 5, $CountPercentThreshold = 75, $ReportThreshold = 50, $ReportFile = &#8220;.\\GroupMembershipComparison.csv&#8221; )<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"jetpack_post_was_ever_published":false,"footnotes":""},"class_list":["post-3171","page","type-page","status-publish","hentry"],"jetpack_shortlink":"https:\/\/wp.me\/Pkp1o-P9","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/pages\/3171","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/comments?post=3171"}],"version-history":[{"count":2,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/pages\/3171\/revisions"}],"predecessor-version":[{"id":3176,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/pages\/3171\/revisions\/3176"}],"wp:attachment":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/media?parent=3171"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}