I'm continuing with my renewed interest in Active Directory, and how I can take advantage of PowerShell. This is a topic I've been working with since the PowerShell v2 days. I have a lot of old code. Some of which I've decided to dust off and polish up. One topic that always interested me, is displaying a visual representation, or a tree, of an Active Directory Domain.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Listing OrganizationalUnits
Listing your OUs isn't especially difficult.
Get-ADOrganizationalUnit -Filter * -Properties canonicalname | Sort-Object -property canonicalname | Select-object -property CanonicalName
The reason I'm including the canonical name, is to make it easier to sort in a hierarchical fashion.
It is easy enough to change the display to use the distinguished name.
Get-ADOrganizationalUnit -Filter * -Properties canonicalname | Sort-Object -property canonicalname | Select-object -property DistinguishedName
But really this is just a list.
Showing a Hierarchy
What I'm missing is a visual representation of the child organizational units. One approach I can take is to start with the root level OUs in the domain, and recursively get the child OUs. I can then format the children to reflect the hierarchy. I can still use Get-ADOrganzationalUnit, but with a few changes.
Here's a parameterized scriptblock.
$get = {param([string]$path,[int]$indent = 2)
$ous = Get-ADOrganizationalUnit -Filter * -Properties canonicalname -SearchBase $path -SearchScope OneLevel
foreach ($child in $ous) {
$tab = "-" * $indent
"|{0} {1}" -f $tab, $child.name
&$get -path $child.distinguishedname -indent ($indent+=2)
$indent-=2
}
}
The script block is getting all OUs in the given location, as specified by the SearchBase parameter. And it is only getting OUs in the current level. That's the SearchScope parameter of OneLevel. I don't want it to recurse. Then for each child I'm building a string with the OU name that is indented to reflect the hierarchy.
I'll run this from the root domain.
(Get-ADDomain).Name;invoke-command $get -ArgumentList "DC=company,dc=pri"
And it would be simple enough to change the code to display the distinguishedname.
This is a pretty good start, but you know I'm going to see how far I can take this.
Show-Domain
I want an easy command to run from a console prompt to generate a domain tree. This means I'll need to write a function. The other change, and this is a major one, is that after discussing this with my friend Gladys Kravitz, I realized there might be times when I also want to include containers like CN=Users.
To get containers, I can't use Get-ADOrganizationalUnit. There is no corresponding container command, but I can use Get-ADObject.
Get-ADObject -ldapfilter "objectclass=container"
By the way, I could also have written this with this filtering syntax.
Get-ADObject -filter "objectclass -eq 'container'"
There's really not much difference between the two.
By default my function will only display organizational units, but if I specify a parameter to show containers, I'll need to modify my LDAP filter accordingly.
if ($Containers) {
$filter = "(|(objectclass=container)(objectclass=organizationalUnit))"
}
else {
$filter = "objectclass=organizationalUnit"
}
My function can then format each child and display indented and with special characters to show tree "branches". I also decided to take advantage of ANSI and show items in different colors.
if ($_.ProtectedFromAccidentalDeletion) {
#display protected OUs in color
$nameValue = "$([char]0x1b)[38;5;199m$name$([char]0x1b)[0m"
}
elseif ($_.objectclass -eq 'container') {
$nameValue = "$([char]0x1b)[38;5;3m$name$([char]0x1b)[0m"
}
elseif ($_.objectclass -ne 'organizationalUnit') {
#display non-OU and non-Container in a different color
$nameValue = "$([char]0x1b)[38;5;211m$name$([char]0x1b)[0m"
}
else {
$nameValue = "$([char]0x1b)[38;5;191m$name$([char]0x1b)[0m"
}
Here's what it looks like.
I can display containers if I want, and I can also use distinguishedname output.
I should point out that this is not an all-inclusive domain tree. I'm filtering out containers with GUID-based names that are under CN=Operations,CN=DomainUpdates,CN=System.
I personally didn't see any value in including them in the output.
The function writes strings to the pipeline, so you can pipe to Out-File. The file contents will include escape characters. Which is great because you and use Get-Content on the file to re-display the colorized tree.
You need to run the function in a PowerShell host that supports ANSI and extended characters. You'll most likely get poor results in the PowerShell ISE or VS Code.
I've posted the function on GitHub as a gist at https://gist.github.com/jdhitsolutions/895d44d68ac4f9523257393d9d4a38d8
The function will let you specify a domain controller and supports alternate credentials. It requires that you have the ActiveDirectory module installed.
Bonus
As I mentioned, I've been scripting with PowerShell and Active Directory for a long time. Here is a function that uses ADSI and the DirectoryServices .NET classes to build a domain tree. This function does NOT require the Active Directory module.
Function Get-DSTree {
[cmdletbinding()]
[OutputType("string")]
Param(
[ADSI]$ADSPath="LDAP://DC=company,DC=Pri",
[int]$Indent=0
)
[string]$leader=" "
[int]$pad = $leader.length+$indent
$searcher = New-Object directoryservices.directorysearcher
$searcher.pagesize=100
#get containers and OUs
#$searcher.Filter = "(|(objectclass=container)(objectclass=organizationalUnit))"
#get only OUs
$Searcher.filter = "(&(objectclass=organizationalUnit))"
$searcher.searchScope="OneLevel"
$searcher.searchRoot = $ADSPath
[void]$searcher.PropertiesToLoad.Add("DistinguishedName")
$searcher.FindAll() | ForEach-Object {
$branch = "{0}{1}" -f ($leader.Padleft($pad,$leader),$_.properties.distinguishedname[0])
$branch
Get-DSTree -ADSPath $_.path -indent ($pad+2)
}
}
I have a few more ideas related to this topic so be sure to follow me on Twitter or subscribe so you don't miss out.
1 thought on “Climbing Trees in PowerShell”
Comments are closed.