Private/Select-ADObject.ps1

function Select-ADObject {
    <#
    .SYNOPSIS
        Displays an interactive GUI dialog to select an Active Directory object from the forest hierarchy.
 
    .DESCRIPTION
        Select-ADObject provides a graphical user interface (GUI) for browsing and selecting Active Directory
        objects such as Organizational Units (OUs), users, groups, and computers. The function displays a
        tree view of the forest structure with custom icons for different object types. It supports browsing
        across multiple domains and optionally highlights Tier 0 (high-security) objects.
 
    .PARAMETER Title
        This text displayed in the dialog window title. Default is "Select AD Object".
 
    .PARAMETER LocalDomainOnly
        If specified, only the current domain and its contents are displayed. By default, all domains
        in the forest are shown.
 
    .PARAMETER DomainSelectionOnly
        If specified, only domain objects can be selected. The tree view is not expanded to show child objects.
 
    .PARAMETER IncludeUsers
        If specified, user objects are included in the browsable tree view.
 
    .PARAMETER IncludeGroups
        If specified, group objects are included in the browsable tree view.
 
    .PARAMETER IncludeComputers
        If specified, computer objects are included in the browsable tree view.
 
    .PARAMETER MarkTier0
        If specified, Tier 0 (high-security) objects are marked with a red overlay on their icons.
        This helps identify highly sensitive AD objects.
 
    .PARAMETER Tier0Regex
        A regular expression pattern used to identify Tier 0 objects. Default pattern matches:
        - CN=Administrator
        - OU=Domain Controllers
        - OU=Tier 0 (case-insensitive)
        - OU=T0 (case-insensitive)
 
    .OUTPUTS
        System.String
        Returns the distinguished name (DN) of the selected Active Directory object, or $null if the
        user cancels the dialog.
 
    .EXAMPLE
        PS> Select-ADObject -Title "Select Target OU"
        Description: Displays the AD selection dialog with a custom title title. Only OUs and containers
        are available for selection.
 
    .EXAMPLE
        PS> Select-ADObject -LocalDomainOnly -IncludeUsers -IncludeGroups
        Description: Shows only the current domain's contents, allowing selection of users and groups
        in addition to OUs.
 
    .EXAMPLE
        PS> Select-ADObject -DomainSelectionOnly
        Description: Restricts the selection to domain objects only.
 
    .EXAMPLE
        PS> Select-ADObject -MarkTier0 -IncludeUsers -Title "Select Tier 0 Target"
        Description: Displays all users and OUs, highlighting Tier 0 objects with a red indicator.
 
    .NOTES
        - Author: zimmermann.holger@live.de
        - Version: 1.0
        - Last Update: 2026-02-22
 
        - This is a private function and requires a graphical environment (Windows Forms).
        - The function uses Active Directory modules to query the forest structure.
        - All logging is handled through Write-Log.
    #>


    [CmdletBinding()]
    param(
        [string]$Title = "Select AD Object",
        [switch]$LocalDomainOnly,
        [switch]$DomainSelectionOnly,
        [switch]$IncludeUsers,
        [switch]$IncludeGroups,
        [switch]$IncludeComputers,
        [switch]$MarkTier0,
        [Switch]$ShowAdvanced,
        [string]$Tier0Regex = '(?i)(^CN=Administrator,|^OU=Domain Controllers,|^OU=Tier\s*0|^OU=T0)'
    )

    $CurrentFunction = "Get-FunctionName"
    Write-Log -Message "### Start Function $CurrentFunction ###"
    $StartRunTime = (Get-Date).ToString($Script:DateFormatLog)
    #################### main code | out- host #####################

    # =========================
    # Assemblies
    # =========================
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing
    Add-Type -AssemblyName System.DirectoryServices
    [System.Windows.Forms.Application]::EnableVisualStyles()

    # =========================
    # Modern Icon Creation
    # =========================

    function New-RoundedRectPath($X, $Y, $W, $H, $R) {
        $path = New-Object System.Drawing.Drawing2D.GraphicsPath
        $path.AddArc($X, $Y, $R, $R, 180, 90)
        $path.AddArc($X + $W - $R, $Y, $R, $R, 270, 90)
        $path.AddArc($X + $W - $R, $Y + $H - $R, $R, $R, 0, 90)
        $path.AddArc($X, $Y + $H - $R, $R, $R, 90, 90)
        $path.CloseFigure()
        $path
    }

    function New-IconBitmap($Kind) {
        $bmp = New-Object System.Drawing.Bitmap 16, 16
        $g = [System.Drawing.Graphics]::FromImage($bmp)
        $g.SmoothingMode = 'AntiAlias'
        $g.Clear([System.Drawing.Color]::Transparent)

        $stroke = New-Object System.Drawing.Pen([System.Drawing.Color]::FromArgb(60, 60, 60), 1)
        $fill = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(220, 226, 232))

        switch ($Kind) {
            'Forest' {

                # Top node (Forest root)
                $g.FillEllipse($fill, 6, 2, 4, 4)
                $g.DrawEllipse($stroke, 6, 2, 4, 4)

                # Left child (Domain)
                $g.FillEllipse($fill, 3, 8, 4, 4)
                $g.DrawEllipse($stroke, 3, 8, 4, 4)

                # Right child (Domain)
                $g.FillEllipse($fill, 9, 8, 4, 4)
                $g.DrawEllipse($stroke, 9, 8, 4, 4)

                # Connection lines
                $g.DrawLine($stroke, 8, 6, 5, 8)
                $g.DrawLine($stroke, 8, 6, 11, 8)
            }
            'Domain' {
                $points = [System.Drawing.Point[]]@(
                    (New-Object System.Drawing.Point 8, 3),
                    (New-Object System.Drawing.Point 13, 12),
                    (New-Object System.Drawing.Point 3, 12)
                )

                $g.FillPolygon($fill, $points)
                $g.DrawPolygon($stroke, $points)
                $g.DrawLine($stroke, 5, 9, 11, 9)
            }
            'OU' {
                $p = New-RoundedRectPath 2 5 12 8 3
                $g.FillPath($fill, $p)
                $g.DrawPath($stroke, $p)
            }
            'Container' {
                $p = New-RoundedRectPath 3 4 10 10 3
                $g.FillPath($fill, $p)
                $g.DrawPath($stroke, $p)
                $g.DrawLine($stroke, 4, 8, 12, 8)
            }
            'User' {
                $g.FillEllipse($fill, 5, 3, 6, 6)
                $g.DrawEllipse($stroke, 5, 3, 6, 6)

                $p = New-RoundedRectPath 3 9 10 5 3
                $g.FillPath($fill, $p)
                $g.DrawPath($stroke, $p)
            }
            'Computer' {
                $p = New-RoundedRectPath 2 3 12 8 2
                $g.FillPath($fill, $p)
                $g.DrawPath($stroke, $p)

                $g.DrawLine($stroke, 6, 12, 10, 12)
                $g.DrawLine($stroke, 8, 11, 8, 14)
            }
            'Group' {
                $g.FillEllipse($fill, 4, 4, 4, 4)
                $g.DrawEllipse($stroke, 4, 4, 4, 4)

                $g.FillEllipse($fill, 8, 4, 4, 4)
                $g.DrawEllipse($stroke, 8, 4, 4, 4)

                $p = New-RoundedRectPath 4 9 8 4 3
                $g.FillPath($fill, $p)
                $g.DrawPath($stroke, $p)
            }
        }
        $g.Dispose()
        $bmp
    }

    function Add-TierOverlay($bmp) {
        $clone = $bmp.Clone()
        $g = [System.Drawing.Graphics]::FromImage($clone)
        $g.FillEllipse([System.Drawing.Brushes]::Red, 10, 10, 5, 5)
        $g.Dispose()
        $clone
    }

    # =========================
    # ImageList
    # =========================

    $imageList = New-Object System.Windows.Forms.ImageList
    $imageList.ImageSize = '16,16'

    $imageList.Images.Add("Forest", (New-IconBitmap Forest)) | Out-Null
    $imageList.Images.Add("Domain", (New-IconBitmap Domain)) | Out-Null
    $imageList.Images.Add("OU", (New-IconBitmap OU)) | Out-Null
    $imageList.Images.Add("Container", (New-IconBitmap Container)) | Out-Null
    $imageList.Images.Add("User", (New-IconBitmap User)) | Out-Null
    $imageList.Images.Add("Computer", (New-IconBitmap Computer)) | Out-Null
    $imageList.Images.Add("Group", (New-IconBitmap Group)) | Out-Null

    if ($MarkTier0) {
        foreach ($k in @("User", "OU", "Container", "Computer", "Group")) {
            $imageList.Images.Add("${k}_T0", (Add-TierOverlay $imageList.Images[$k])) | Out-Null
        }
    }

    # =========================
    # Helpers
    # =========================

    function Convert-DomainToDN($d) { 'DC=' + ($d -replace '\.', ',DC=') }

    function Get-ForestDomains {
        ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).Domains |
        Select-Object -ExpandProperty Name
    }

    function Get-ChildNodes($node) {

        if ($DomainSelectionOnly) { return }

        if ($node.Nodes.Count -eq 1 -and $node.Nodes[0].Text -eq 'dummy') {

            $node.Nodes.Clear()

            $root = [ADSI]"LDAP://$($node.Name)"
            $searcher = New-Object System.DirectoryServices.DirectorySearcher($root)
            $searcher.SearchScope = 'OneLevel'
            
            $filters = @(
                "(objectClass=organizationalUnit)",
                "(objectClass=container)"
                "(objectClass=builtinDomain)"
            )

            if ($IncludeUsers) { $filters += "(objectCategory=user)" }
            if ($IncludeGroups) { $filters += "(objectCategory=group)" }
            if ($IncludeComputers) { $filters += "(objectCategory=computer)" }

            $orFilter = "(|" + ($filters -join "") + ")"

            if (-not $ShowAdvanced) {
                $searcher.Filter = "(& $orFilter (!(showInAdvancedViewOnly=TRUE)))"
            }
            else {
                $searcher.Filter = $orFilter
            }
          
            write-verbose $orFilter
          
            $null = $searcher.PropertiesToLoad.AddRange(@("name", "distinguishedName", "objectClass"))

            foreach ($res in $searcher.FindAll()) {

                $dn = $res.Properties.distinguishedname[0]
                $name = $res.Properties.name[0]
                $cls = $res.Properties.objectclass[-1]
                # change icon here
                #$iconKey = if ($cls -eq "organizationalUnit") { "OU" } else { "User" }

                $iconKey = switch ($cls) {
                    'organizationalUnit' { 'OU' }
                    'container' { 'Container' }
                    'user' { 'User' }
                    'group' { 'Group' }
                    'computer' { 'Computer' }
                    default { 'Container' }
                }

                if ($MarkTier0 -and $dn -match $Tier0Regex -and $imageList.Images.ContainsKey("${iconKey}_T0")) {
                    $iconKey = "${iconKey}_T0"
                }

                $child = New-Object System.Windows.Forms.TreeNode
                $child.Text = $name
                $child.Name = $dn
                $child.ImageKey = $iconKey
                $child.SelectedImageKey = $iconKey
                $child.Nodes.Add("dummy") | Out-Null

                $node.Nodes.Add($child) | Out-Null
            }
        }
    }

    # =========================
    # FORM
    # =========================

    $form = New-Object System.Windows.Forms.Form
    $form.Text = $Title
    $form.Size = '520,700'
    $form.StartPosition = 'CenterScreen'
    $form.FormBorderStyle = 'FixedDialog'
    $form.MaximizeBox = $false
    $form.MinimizeBox = $false
    $form.Padding = '10,10,10,10'

    $font = New-Object System.Drawing.Font("Segoe UI", 9)

    $tree = New-Object System.Windows.Forms.TreeView
    $tree.Location = '10,10'
    $tree.Size = '480,560'
    $tree.Font = $font
    $tree.ImageList = $imageList

    $statusLabel = New-Object System.Windows.Forms.Label
    $statusLabel.Location = '10,580'
    $statusLabel.Size = '480,20'
    $statusLabel.Text = "Selected: none"
    $statusLabel.Font = $font

    $btnCancel = New-Object System.Windows.Forms.Button
    $btnCancel.Text = "Cancel"
    $btnCancel.Location = '310,610'
    $btnCancel.Size = '80,28'
    $btnCancel.Font = $font

    $btnOK = New-Object System.Windows.Forms.Button
    $btnOK.Text = "OK"
    $btnOK.Location = '410,610'
    $btnOK.Size = '80,28'
    $btnOK.Font = $font
    $btnOK.Enabled = $false

    $form.AcceptButton = $btnOK
    $form.CancelButton = $btnCancel

    # =========================
    # Build Forest
    # =========================

    $forestName = ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).Name

    $forestNode = New-Object System.Windows.Forms.TreeNode
    $forestNode.Text = $forestName
    $forestNode.ImageKey = "Forest"
    $forestNode.SelectedImageKey = "Forest"
    $tree.Nodes.Add($forestNode) | Out-Null

    $domains = Get-ForestDomains
    $currentDomain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name

    foreach ($d in $domains) {

        if ($LocalDomainOnly -and $d -ne $currentDomain) { continue }

        $dn = Convert-DomainToDN $d

        $domainNode = New-Object System.Windows.Forms.TreeNode
        $domainNode.Text = $d
        $domainNode.Name = $dn
        $domainNode.ImageKey = "Domain"
        $domainNode.SelectedImageKey = "Domain"
        $domainNode.Nodes.Add("dummy") | Out-Null

        $forestNode.Nodes.Add($domainNode) | Out-Null
    }

    $forestNode.Expand()

    # =========================
    # Events
    # =========================

    $tree.add_BeforeExpand({ Get-ChildNodes $_.Node })

    $tree.add_AfterSelect({
            $node = $_.Node
            $statusLabel.Text = "Selected: $($node.Text)"

            if ($DomainSelectionOnly) {
                $btnOK.Enabled = ($node.Parent -and -not $node.Parent.Parent)
                return
            }

            if ($AllowDomainSwitch) {
                $btnOK.Enabled = [bool]($node.Parent)
            }
            else {
                $btnOK.Enabled = [bool]($node.Parent -and $node.Parent.Parent)
            }
        })

    $btnOK.add_Click({
            $form.Tag = $tree.SelectedNode.Name
            $form.Close()
        })

    $btnCancel.add_Click({
            $form.Tag = $null
            $form.Close()
        })

    $form.Controls.AddRange(@($tree, $statusLabel, $btnCancel, $btnOK))

    $null = $form.ShowDialog()

    Write-Log -Message " >> AD Object selected: $($form.Tag)"
    ######################## main code ############################
    $runtime = Get-RunTime -StartRunTime $StartRunTime
    Write-Log -Message " Run Time: $runtime [h] ###"
    Write-Log -Message "### End Function $CurrentFunction ###"

    return $form.Tag
}