2

I have been trying to create a script to map out an Active Directory's OU structures. (The end goal would be to have the equivalent of the windows tree.com command.)

For the following folder structure:

  • Domain.local
    • Domain Controllers
    • Users
      • Internal
      • External

Would have the following Output:

[Domain.local]
Domain Controllers
Users
  Internal
  External

The code used to do it would be the following:

function Display-OU{
    param(
        [String]$ParentOU,
        [System.Collections.ArrayList]$ChildOUs
    )
    # $ChildOUs
    $ChildOUs | ForEach-Object{$_.DN.Remove($_.Name)}
    if($ParentOU){
        $Spacing = "  "
    }else{
        $Spacing = ""
    }
    $Return = foreach($cOU in $ChildOUs){
        if($cOU.DN){
            Continue
        }
        $Parent = $cOU.Name
        "$Spacing$($cOU.Name)"
        $Children = $ChildOUs | Where-Object{$_.DN -contains $Parent}
        if($Children){
            "$Spacing$(Display-OU -ParentOU $ParentOU  -ChildOUs $Children)"
        }
    }
    return $Return
}

# Recover the OU data
$Raw = Get-ADOrganizationalUnit -Filter * | Select Name,DistinguishedName
# Clean up the data
$OUs = foreach($Entry in $Raw){
    [PsCustomObject]@{
        Name = $Entry.Name
        Domain = ($Entry.DistinguishedName.Split(",") | Where-Object{$_ -match "DC="}) -join "." -replace "DC="
        DN = [System.Collections.ArrayList](($Entry.DistinguishedName -replace '\,DC=.+').Split(",") -replace "OU=")
    }
}

# Run the command depending on the number of domains
$Domains = $OUs.Domain | Select -Unique
foreach($D in $Domains){
    $dOUs = $OUs | Where-Object{$_.Domain -eq $D}
    Write-Host "[$D]"
    Display-OU -ChildOUs $dOUs
}

I am guessing I am doing something wrong when defining the variables, because I keep getting the following Output:

[testdomain.local]
Domain Controllers
Test_Domain_Users

As far as I understand, it runs the first time but fails to call itself again...

2
  • 1
    This would be a good opportunity to learn how to use a debugger. Single-step through the code, observe variable values and check if they meet your assumptions. E. g. is the $Children variable not empty. VSCode with the PowerShell extension provides a really nice debugging experience. Commented Nov 28, 2022 at 22:02
  • @zett42, I have already tried doing so. When running the contents of the function run manually, it still fails to call Display-OU -ParentOU $Parent -ChildOUs $Children. Despite confirming that both variables $Parent and $Children are populated. Commented Nov 29, 2022 at 19:09

1 Answer 1

2

This is a quite fun exercise, I can show you how I would do it with a Stack<T> instead of recursion and a custom class. Most of the logic is re-used from this module.

using namespace System.Collections.Generic
using namespace Microsoft.ActiveDirectory.Management
using namespace System.Management.Automation

function Get-TreeOrganizationalUnit {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Identity,

        [Parameter()]
        [switch] $IncludeContainer
    )

    end {
        $filter = "(|(name=$Identity)(samAccountName=$Identity)(distinguishedName=$Identity))"
        $object = Get-ADObject -LDAPFilter $filter

        if(-not $object) {
            return $PSCmdlet.WriteWarning("Cannot find an object with Identity: '$Identity'.")
        }

        if($object.Count -gt 1) {
            $errorRecord = [ErrorRecord]::new(
                [Exception] "More than one object found with Identity: '$Identity'. Please use 'DistinguishedName' attribute.",
                [string] "AmbiguousResult",
                [ErrorCategory]::NotImplemented,
                $Identity
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        class Tree {
            [string] $Hierarchy
            [string] $ObjectClass
            hidden [int] $Depth
            hidden [string] $Base

            Tree([ADObject] $Object, [int] $Depth) {
                $this.Hierarchy   = [Tree]::Indent($Object.Name, $Depth)
                $this.ObjectClass = $Object.ObjectClass
                $this.Base        = $Object.DistinguishedName
                $this.Depth       = $Depth
            }

            static [string] Indent([string] $String, [Int64] $Indentation) {
                return "$('    ' * $Indentation)$String"
            }

            static [void] DrawTree([object[]] $InputObject, [PSCmdlet] $Cmdlet) {
                $corner, $horizontal, $pipe, $connector = '└', '─', '│', '├'

                $cornerConnector = "${corner}$(${horizontal}*2) "
                foreach($group in $InputObject | Group-Object Depth | Select-Object -Skip 1) {
                    foreach($item in $group.Group) {
                        $item.Hierarchy = $item.Hierarchy -replace '\s{4}(?=\S)', $cornerConnector
                    }
                }

                for($i = 1; $i -lt $InputObject.Count; $i++) {
                    $index = $InputObject[$i].Hierarchy.IndexOf($corner)
                    if($index -ge 0) {
                        $z = $i - 1
                        while($InputObject[$z].Hierarchy[$index] -notmatch "$corner|\S") {
                            $replace = $InputObject[$z].Hierarchy.ToCharArray()
                            $replace[$Index] = $pipe
                            $InputObject[$z].Hierarchy = [string]::new($replace)
                            $z--
                        }

                        if($InputObject[$z].Hierarchy[$index] -eq $corner) {
                            $replace = $InputObject[$z].Hierarchy.ToCharArray()
                            $replace[$Index] = $connector
                            $InputObject[$z].Hierarchy = [string]::new($replace)
                        }
                    }
                }
                $Cmdlet.WriteObject($InputObject, $true)
            }
        }

        $stack = [Stack[Tree]]::new()
        $stack.Push([Tree]::new($object, 0))

        $param = @{
            LDAPFilter  = '(objectClass=organizationalUnit)'
            SearchScope = 'OneLevel'
        }

        if($IncludeContainer.IsPresent) {
            $param['LDAPFilter'] = '(|(objectClass=container)' + $param['LDAPFilter'] + ')'
        }

        [Tree]::DrawTree(@(
            while($stack.Count) {
                $target = $stack.Pop()
                $target
                $param['SearchBase'] = $target.Base
                foreach($object in Get-ADObject @param) {
                    $stack.Push([Tree]::new($object, $target.Depth + 1))
                }
            }
        ), $PSCmdlet)
    }
}

The usage is straight forward, the -Identity can be either the SamAccountName or DistinguishedName attribute of the initial object (the object that is used as base):

# Only OrganizationalUnits
Get-TreeOrganizationalUnit -Identity myDomain

# Includes Containers too
Get-TreeOrganizationalUnit -Identity myDomain -IncludeContainer

Output would look something like this:

Hierarchy              ObjectClass
---------              -----------
myDomain               domainDNS
├── Workstations       organizationalUnit
├── SomeOU             organizationalUnit
│   ├── OtherOU1       organizationalUnit
│   └── OtherOU2       organizationalUnit
├── TestOU             organizationalUnit
├── People             organizationalUnit
├── Operations         organizationalUnit
└── Domain Controllers organizationalUnit
Sign up to request clarification or add additional context in comments.

2 Comments

First, great script, does exactly what I intended to do. But I have to admit I am a bit lost when reading it. Is this still PowerShell or is it C#? And although you did give me a solution for visualizing the architecture of an Active Directory, I don't know if I can mark it as an answer to the given question...
@AlD.J. I know, the code is not easy to follow, 100% understandable, but it's also hard to explain. I'm fine you decide not to accept it, my intent from the beginning was to share a different approach, I don't like recursion when it can be avoided (it's pretty slow). This is PowerShell btw, tho it can look similar to C# when it comes to classes :P

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.