Discover Circular AD Group Memberships

Might as well have a recursive function in the first post eh?

I stumbled across this script to discover circular Active Directory group memberships but was inspired to get a more visual representation after reading the FAQ section.

Group membership details used for demonstration:

Script output comparison:

 

The script will pull in all AD groups in your domain, check for circular links using memberOf discovery, and then output results.  Due to the worst case scenario when dealing with circular group membership the script won’t discover all potential paths during the first pass.  Continue running until no circular groups are discovered.

# Copyright (c) 2017 infiniteloop.io
# http://infiniteloop.io
# Version 1.0 - 2017-02-01
# 
# Redistribution and use in source and binary forms, with or without modification, are permitted 
# provided that the following conditions are met:
# 1) Redistributions of source code must retain the above copyright notice, this list of conditions 
#    and the following disclaimer.
# 2) Redistributions in binary form must reproduce the above copyright notice, this list of 
#    conditions and the following disclaimer in the documentation and/or other materials provided 
#    with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ''AS IS'' AND ANY EXPRESS OR IMPLIED 
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


#region FUNCTIONS

function Find-CircularGroupMembership
{
  [CmdletBinding()]
  Param 
  (
    [Parameter(Mandatory=$true,Position=0)]
    [ValidateNotNullOrEmpty()]
    [string]$GroupDN,

    [Parameter(Mandatory=$false,Position=1)]
    [string]
    $ParentGroupDN="",

    [Parameter(Mandatory=$false,Position=2)]
    [string]$NestDetails=""
  )

  Write-Verbose "Checking $GroupDN"

  if($global:enumeratedGroups -contains $GroupDN){Write-Debug "Previously enumerated group, skip to prevent loop" ; return}
  
  $path = $NestDetails + ($GroupDN.Split(',')[0]).replace('CN=','') +"->"
 
  if($GroupDN -eq $global:origGroup.DistinguishedName -and $ParentGroupDN -ne "") 
  {
    Write-Debug "Circular Group Membership found"
    Write-Debug "Orig Group: $($global:origGroup.Name)"
    Write-Debug "Last Group nested into Orig Group: $ParentGroupDN"
    Write-Debug $path.trim("->")
    
    $props = @{
      'OrigGroup' = $global:origGroup.Name
      'LastGroupNestedIntoOrigGroup' = (Get-ADGroup $ParentGroupDN).name
      'NestDetails' = $path.trim("->")
    }
    New-Object -TypeName PSobject -Property $props
  } 
  else 
  {
    if($ParentGroupDN -ne ""){$global:enumeratedGroups += $groupDN}

    $memberOfArr = @()
    $memberOfArr += try {(Get-ADGroup $GroupDN -Properties MemberOf) | ? {$_.MemberOf.Count -gt 0}| % {$_.memberof}} catch {$null}

    if(@($memberOfArr).count -gt 0) 
    {
      $memberOfArr  | % {Find-CircularGroupMembership -GroupDN $_ -ParentGroupDN $GroupDN -NestDetails $path}
    } 
    else 
    {
      Write-Debug "No MemberOf groups present"
    }
  }
}

#endregion FUNCTIONS



#region MAIN

try {ipmo ActiveDirectory -ea stop} catch {Write-Host $_.exception.message -f Red; break}

$arr = try {Get-ADGroup -f * -ea stop} catch {Write-Host $_.exception.message -f Red}

if($arr) 
{
  Write-Host "Groups to check:`t$($arr.count)"
  $report = @()
  $total = $arr.count
  $count = 1
    
  foreach($group in $arr) 
  {
    Write-Progress -Activity "Checking $($group.name)" -PercentComplete (($count / $total) * 100)

    $global:origGroup = $group     #maintains the original group sent to recursive function
    $global:enumeratedGroups = @() #tracks groups enumerated by recursive function to prevent infinite loop if a circular membership is found
    
    $report += Find-CircularGroupMembership -GroupDN $group.DistinguishedName
    $count++
  }

  if($report.count -gt 0) {$report | ft -a} else {Write-Host "No Circular Groups Discovered" -f Green}
}

#endregion MAIN