5
\$\begingroup\$

I've just published ADExploder. It's a single-script PowerShell app (source) that uses classic Windows Forms and the Directory Services API to be able to browse nodes and properties of Active Directory servers.

I am interested in hearing feedback about my use of PowerShell, whether it can be made more terse or idiomatic, ways that I can improve my usage of either the Windows Forms or Active Directory API, and any other improvements you can think of.

The full body of the script is here:

# Greg Toombs, November 2013

add-type -a (
    'System.DirectoryServices',
    'System.DirectoryServices.AccountManagement',
    'System.Drawing',
    'System.Windows.Forms'
)

function GetConnDlg() {
    $label = new-object Windows.Forms.Label -pr @{
        AutoSize = $true
        Dock = [Windows.Forms.DockStyle]::Top
        Text = 'Hostname'
    }
    $text = new-object Windows.Forms.TextBox -pr @{
        Anchor = [Windows.Forms.AnchorStyles] 'Left, Right, Top'
        MinimumSize = new-object Drawing.Size -a 120, 20
    }
    $ok = new-object Windows.Forms.Button -pr @{
        DialogResult = [Windows.Forms.DialogResult]::OK
        Text = '&Connect'
    }
    $cancel = new-object Windows.Forms.Button -pr @{
        DialogResult = [Windows.Forms.DialogResult]::Cancel
        Text = 'C&ancel'
    }
    $flowh = new-object Windows.Forms.FlowLayoutPanel -pr @{
        AutoSize = $true
        Dock = [Windows.Forms.DockStyle]::Bottom
        FlowDirection = [Windows.Forms.FlowDirection]::RightToLeft
    }
    $flowh.Controls.AddRange(($cancel, $ok))
    $flowv = new-object Windows.Forms.FlowLayoutPanel -pr @{
        AutoSize = $true
        Dock = [Windows.Forms.DockStyle]::Fill
        FlowDirection = [Windows.Forms.FlowDirection]::TopDown
    }
    $flowv.Controls.AddRange(($label, $text))
    $conndlg = new-object Windows.Forms.Form -pr @{
        AcceptButton = $ok
        CancelButton = $cancel
        MaximizeBox = $false
        MinimizeBox = $false
        Padding = new-object Windows.Forms.Padding -a 5
        Size = new-object Drawing.Size -a 230, 130
        StartPosition = [Windows.Forms.FormStartPosition]::CenterParent
        Text = 'Connect'
    }
    $conndlg.Controls.AddRange(($flowv, $flowh))
    $ok.Add_Click({
        $conndlg.Tag = $text.Text
        $text.Text = ''
    }.GetNewClosure())
    $conndlg
}

function GetProp($obj, $prop) {
    $obj.GetType().InvokeMember(
        $prop, [Reflection.BindingFlags]::GetProperty, $null, $obj, $null)
}

function IsSaneDate($dt) {
    $dt.Year -ge 1800 -and $dt.Year -lt 2200
}

function ByteProp($propname, $val, $add, $expand) {
    if ($val -is [Byte[]]) {
        $tname = $val.GetType().FullName
        if ($val.Length -eq 16) {
            &$add $propname "$tname (GUID)" (new-object Guid -a (,$val))
        }
        elseif ($val.Length -eq 28) {
            &$add $propname "$tname (SID)" (new-object Security.Principal.SecurityIdentifier -a (,$val,0))
        }
        elseif ('replUpToDateVector', 'repsFrom', 'repsTo' -contains $propname -and -not $expand) {
            &$add $propname $tname $val $true
        }
        else {
            &$add $propname $tname $([BitConverter]::ToString($val))
        }
        $true
    }
}

function I64Prop($propname, $val, $add, $expand) {
    if ($val -is [__ComObject]) {
        try {
            [int64] $hi = GetProp $val 'HighPart'
            [int64] $lo = GetProp $val 'LowPart'
            [int64] $qword = ($hi -shl 32) -bor $lo
            $valstr = [string]::Format('{0} = 0x{0:X16}', $qword)
            try {
                $dt = [DateTime]::FromFileTime($qword)
                if (IsSaneDate $dt) {
                    $valstr = [string]::Format("{0} = {1}", $valstr, $dt)
                }
            } catch { }
            try {
                if ('lockoutDuration', 'forceLogoff', 'lockOutObservationWindow',
                    'maxPwdAge', 'minPwdAge' -contains $propname `
                    -and $qword -ne 0x8000000000000000) {
                    $ts = [TimeSpan]::FromTicks([math]::abs($qword))
                    $valstr = [string]::Format("{0} = {1}", $valstr, $ts)
                }
            } catch { }
            &$add $propname 'ActiveDs.LargeInteger' $valstr
            $true
        } catch { }
    }
}

function AccessControlProp($propname, $val, $add, $expand) {
    if ($val -is [__ComObject]) {
        try {
            $valstr = [string]::Format(@'
AccessMask = {0} = 0x{0:X8}
AceFlags = {1} = 0x{1:X8}
AceType = {2} = 0x{2:X8}
Flags = {3} = 0x{3:X8}
InheritedObjectType = {4}
ObjectType = {5}
Trustee = {6}
'@,                $(GetProp $val 'AccessMask'),
                $(GetProp $val 'AceFlags'),
                $(GetProp $val 'AceType'),
                $(GetProp $val 'Flags'),
                $(GetProp $val 'InheritedObjectType'),
                $(GetProp $val 'ObjectType'),
                $(GetProp $val 'Trustee'))
            &$add $propname 'ActiveDs.AccessControlEntry' $valstr
            $true
        } catch { }
    }
}

function SecDescProp($propname, $val, $add, $expand) {
    if ($val -is [__ComObject]) {
        try {
            $valstr = [string]::Format(@'
Control = {0} = 0x{0:X8}
DaclDefaulted = {1}
Group = {2}
GroupDefaulted = {3}
Owner = {4}
OwnerDefaulted = {5}
Revision = {6}
SaclDefaulted = {7}
'@,                $(GetProp $val 'Control'),
                $(GetProp $val 'DaclDefaulted'),
                $(GetProp $val 'Group'),
                $(GetProp $val 'GroupDefaulted'),
                $(GetProp $val 'Owner'),
                $(GetProp $val 'OwnerDefaulted'),
                $(GetProp $val 'Revision'),
                $(GetProp $val 'SaclDefaulted'))
            &$add $propname 'ActiveDs.SecurityDescriptor' $valstr
            &$add "$propname.DiscretionaryAcl" 'ActiveDs.AccessControlEntry[]' `
                $(GetProp $val 'DiscretionaryAcl') $true
            $(GetProp $val 'SystemAcl') | %{
                AccessControlProp "$propname.SystemAcl" $_ $add
            }
            $true
        } catch { }
    }
}

function DNProp($propname, $val, $add, $expand) {
    if ($val -is [__ComObject]) {
        try {
            $valstr = [string]::Format("BinaryValue = {0}`nDNString = {1}",
                $(new-object Guid -a (,[byte[]]$(GetProp $val 'BinaryValue'))),
                $(GetProp $val 'DNString'))
            &$add $propname 'ActiveDs.DNWithBinary' $valstr
            $true
        } catch { }
    }
}

function IntProp($propname, $val, $add, $expand) {
    if ($val -is [int]) {
        &$add $propname $($val.GetType().FullName) `
            $([string]::Format('{0} = 0x{0:X8}', $val))
        $true
    }
}

function DateTimeProp($propname, $val, $add, $expand) {
    if ($val -is [DateTime] -and -not(IsSaneDate $val)) {
        &$add $propname $($val.GetType().FullName) `
            $([string]::Format('{0} = 0x{0:X16}', $val.ToFileTime()))
        $true
    }
}

function AddRowsFor($details, $propname, $val, $expand) {
    if ($val -is [Collections.IEnumerable] -and `
        $val -isnot [string] -and $val -isnot [byte[]]) {
        $val.GetEnumerator() | %{ AddRowsFor $details $propname $_ }
        return
    }
    $add = {
        param($pname, $typestr, $valstr, $delayload)
        if ($delayload) {
            $rindex = $details.Rows.Add($pname, $typestr, '[Double-click to show]')
            $details.Rows[$rindex].Tag = $valstr
        }
        else {
            $details.Rows.Add($pname, $typestr, $valstr)
        }
    }.GetNewClosure()
    if (ByteProp $propname $val $add $expand) { return }
    if (I64Prop $propname $val $add $expand) { return }
    if (SecDescProp $propname $val $add $expand) { return }
    if (AccessControlProp $propname $val $add $expand) { return }
    if (DNProp $propname $val $add $expand) { return }
    if (IntProp $propname $val $add $expand) { return }
    if (DateTimeProp $propname $val $add $expand) { return }
    &$add $propname $val.GetType().FullName $val `
        $($propname -eq 'gPLink' -and -not $expand)
}

function NewNode($name, $dn) {
    $node = new-object Windows.Forms.TreeNode -a $name -pr @{
        Tag = "LDAP://$dn"
    }
    [void] $node.Nodes.Add($(new-object Windows.Forms.TreeNode `
        -a 'Loading...' -pr @{ Name = 'LoadingNode' }))
    $node
}

function GetWindow() {
    $AddRowsFor = gi function:AddRowsFor
    $NewNode = gi function:NewNode
    $conndlg = GetConnDlg

    $connectcmd = new-object Windows.Forms.ToolStripMenuItem -pr @{
        ShortcutKeys = [Windows.Forms.Keys] 'Control, O'
        Text = '&Open'
    }
    $quitcmd = new-object Windows.Forms.ToolStripMenuItem -pr @{
        ShortcutKeys = [Windows.Forms.Keys] 'Alt, F4'
        Text = '&Quit'
    }
    $filemenu = new-object Windows.Forms.ToolStripMenuItem -pr @{
        Text = '&File'
    }
    $filemenu.DropDownItems.AddRange(($connectcmd, $quitcmd))
    $menu = new-object Windows.Forms.MenuStrip
    $menu.Items.AddRange(($filemenu))

    $window = new-object Windows.Forms.Form -pr @{
        ClientSize = new-object Drawing.Size -a 600, 400
        MainMenuStrip = $menu
        Text = 'AD Exploder'
    }
    $quitcmd.Add_Click({ $window.Close() }.GetNewClosure())

    $details = new-object Windows.Forms.DataGridView -pr @{
        AllowUserToAddRows = $false
        AllowUserToDeleteRows = $false
        AutoSizeRowsMode = [Windows.Forms.DataGridViewAutoSizeRowsMode]::AllCells
        AutoSizeColumnsMode = [Windows.Forms.DataGridViewAutoSizeColumnsMode]::AllCells
        Dock = [Windows.Forms.DockStyle]::Fill
        ReadOnly = $true
    }
    'Name', 'Type', 'Value' | %{ [void] $details.Columns.Add(
        $(new-object Windows.Forms.DataGridViewTextBoxColumn -pr @{
            HeaderText = $_
            Name = $_
        })
    )}
    $details.Columns['Value'].DefaultCellStyle = new-object Windows.Forms.DataGridViewCellStyle -pr @{
        WrapMode = [Windows.Forms.DataGridViewTriState]::True
    }
    $details.Add_CellDoubleClick({
        param($sender, $e)
        if ($e.ColumnIndex -eq 2) {
            $data = $details.Rows[$e.RowIndex].Tag
            if ($data -ne $null) {
                &$AddRowsFor $details $details[0,$e.RowIndex].Value $data $true
                $details.Rows.RemoveAt($e.RowIndex)
                $details.Sort($details.Columns['Name'], [ComponentModel.ListSortDirection]::Ascending)
            }
        }
    }.GetNewClosure())

    $tree = new-object Windows.Forms.TreeView -pr @{
        Dock = [Windows.Forms.DockStyle]::Fill
    }
    $tree.Add_AfterSelect({
        param($sender, $e)
        $adroot = new-object DirectoryServices.DirectoryEntry -a $e.Node.Tag
        $details.Rows.Clear()
        $adroot.Properties.GetEnumerator() | %{
            &$AddRowsFor $details $_.PropertyName $_.Value
            [Windows.Forms.Application]::DoEvents()
        }
        $details.Sort($details.Columns['Name'], [ComponentModel.ListSortDirection]::Ascending)
    }.GetNewClosure())
    $tree.Add_AfterExpand({
        param($sender, $e)
        if ($e.Node.Nodes.Count -eq 1 -and $e.Node.Nodes[0].Name -eq 'LoadingNode') {
            $adroot = new-object DirectoryServices.DirectoryEntry -a $e.Node.Tag
            $adroot.Children | %{
                [void] $e.Node.Nodes.Add($(NewNode $_.Name $_.distinguishedName))
                [Windows.Forms.Application]::DoEvents()
            }
            $e.Node.Nodes.RemoveByKey('LoadingNode')
        }
    })

    $Connect = {
        param($to)
        $node = &$NewNode $to $to
        $tree.Nodes.Add($node)
        $tree.SelectedNode = $node
    }.GetNewClosure()
    $connectcmd.Add_Click({
        if ($conndlg.ShowDialog() -eq [Windows.Forms.DialogResult]::OK) {
            &$Connect $conndlg.Tag
        }
    }.GetNewClosure())
    $window.Add_Shown({
        $connserv = [Environment]::GetEnvironmentVariable('logonserver')
        if ($connserv) {
            if ($connserv.StartsWith('\\')) {
                $connserv = $connserv.Substring(2)
            }
            &$Connect $connserv
        }
    }.GetNewClosure())

    $split = new-object Windows.Forms.SplitContainer -pr @{
        Dock = [Windows.Forms.DockStyle]::Fill
    }
    $split.Panel1.Controls.Add($tree)
    $split.Panel2.Controls.Add($details)
    $window.Controls.AddRange(($split, $menu))
    $window
}

[void] (GetWindow).ShowDialog()
\$\endgroup\$
  • \$\begingroup\$ An AD Exploder sounds like just the tool I need.... ;-) ! \$\endgroup\$ – rolfl Jun 23 '14 at 19:31
  • \$\begingroup\$ The name "AD Explorer" was already taken :-P \$\endgroup\$ – Reinderien Jun 23 '14 at 19:32
3
\$\begingroup\$

Your code looks pretty good to me.


You can replace this:

[string]::Format('{0} = 0x{0:X8}', $val)

with this:

'{0} = 0x{0:X8}' -f $val

Empty "catch all" catch blocks are not a good idea:

    try {
       # ...
    } catch { }

They will hide all exception types indiscriminately, including ones that you might want to know about, which is not a good thing.

If you want to ignore a particular exception type, you should catch just that type (and ideally there should be comment saying why you are ignoring it):

    try {
       # ...
    } catch [SomeExceptionType] { 
       # Ignored because blah.
    }

This is a good thing to put at the top of scripts IMO:

Set-StrictMode -Version Latest

It will catch various programming mistakes that might otherwise pass unnoticed.

\$\endgroup\$
  • \$\begingroup\$ Awesome suggestions. The -f and the set-strictmode are already in; as to the exceptions I have to go back and re-examine what I was thinking at the time. \$\endgroup\$ – Reinderien Jun 24 '14 at 16:45
  • \$\begingroup\$ @Reinderien, cool, glad you found the suggestions useful. \$\endgroup\$ – Dangph Jun 27 '14 at 0:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.