#Requires -Modules ActiveDirectory Add-Type -AssemblyName System.Windows.Forms # Create a form $form = New-Object System.Windows.Forms.Form $form.Text = "Select an OU" $form.Width = 500 $form.Height = 400 $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen # Create a TreeView $treeView = New-Object System.Windows.Forms.TreeView $treeView.Width = 450 $treeView.Height = 300 $treeView.Location = New-Object System.Drawing.Point(20, 20) $treeView.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right # Function to dynamically load sub-OUs when expanding a node function LoadSubOUs { param ($node) # Get the parent OU's Distinguished Name from the node's Tag property $parentOU = $node.Tag # Clear existing nodes (like the dummy node) before adding children $node.Nodes.Clear() Write-Host "DEBUG: Loading OUs under '$parentOU'" try { # Retrieve *only direct child OUs* using -SearchScope OneLevel $subOUs = Get-ADOrganizationalUnit -Filter * -SearchBase $parentOU -SearchScope OneLevel -Property DistinguishedName, Name -ErrorAction Stop | Select-Object DistinguishedName, Name # Check if any sub-OUs were found if ($subOUs -ne $null) { # Sort OUs alphabetically by name for better readability $subOUs = $subOUs | Sort-Object Name foreach ($OU in $subOUs) { $subNode = New-Object System.Windows.Forms.TreeNode $subNode.Text = $OU.Name # Display the friendly OU name $subNode.Tag = $OU.DistinguishedName # Store the full DN for later use $node.Nodes.Add($subNode) # Add a dummy node to indicate potential expandability. # The BeforeExpand event will handle loading actual children if this node is expanded. $subNode.Nodes.Add("") } Write-Host "DEBUG: Found $($subOUs.Count) sub-OUs under '$parentOU'" } else { Write-Host "DEBUG: No sub-OUs found under '$parentOU'" } } catch { Write-Warning "Error loading OUs under '$parentOU': $($_.Exception.Message)" # Optionally add an error node or message to the UI # $errorNode = New-Object System.Windows.Forms.TreeNode # $errorNode.Text = "Error loading children" # $node.Nodes.Add($errorNode) } } # Handle Node Expansion to Load Sub-OUs $treeView.add_BeforeExpand({ param($sender, $e) $expandedNode = $e.Node # Check if the node contains only the dummy node, indicating it hasn't been loaded yet. # This prevents reloading if the node was expanded, collapsed, and expanded again, # although the current LoadSubOUs with Clear() handles this too. if ($expandedNode.Nodes.Count -eq 1 -and $expandedNode.Nodes[0].Text -eq "") { Write-Host "DEBUG: Expanding node '$($expandedNode.Text)' Tag:'$($expandedNode.Tag)'" # Use BeginUpdate/EndUpdate for smoother UI experience when adding multiple nodes $treeView.BeginUpdate() LoadSubOUs -node $expandedNode $treeView.EndUpdate() } elseif ($expandedNode.Nodes.Count -gt 0 -and $expandedNode.Nodes[0].Text -ne "") { Write-Host "DEBUG: Node '$($expandedNode.Text)' already loaded." } elseif ($expandedNode.Nodes.Count -eq 0) { # This case might happen if LoadSubOUs previously ran but found no children. # We might want to re-run LoadSubOUs just in case something changed, # or simply do nothing as there were no children last time. Write-Host "DEBUG: Node '$($expandedNode.Text)' previously expanded and found no children." # Optional: Uncomment to force reload on re-expand even if no children were found previously # $treeView.BeginUpdate() # LoadSubOUs -node $expandedNode # $treeView.EndUpdate() } }) # Get the root domain DN try { $rootDomain = Get-ADDomain -ErrorAction Stop $rootOU = $rootDomain.DistinguishedName $domainNode = New-Object System.Windows.Forms.TreeNode # Display a friendlier name if possible (e.g., the NetBIOS name) $domainNode.Text = $rootDomain.NetBIOSName + " ($rootOU)" $domainNode.Tag = $rootOU $treeView.Nodes.Add($domainNode) # Add a dummy node to indicate expandability $domainNode.Nodes.Add("") Write-Host "Root OU: $rootOU" } catch { Write-Error "Failed to retrieve AD Domain information. Ensure the Active Directory module is available and you have permissions. Error: $($_.Exception.Message)" [System.Windows.Forms.MessageBox]::Show("Failed to retrieve AD Domain information. Exiting script.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) exit } # Create an "OK" button $okButton = New-Object System.Windows.Forms.Button $okButton.Text = "OK" $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK # Set DialogResult $okButton.Width = 80 # Calculate the X coordinate separately to avoid type confusion during object creation $buttonX = ($form.ClientSize.Width - $okButton.Width) / 2 $okButton.Location = New-Object System.Drawing.Point($buttonX, 330) $okButton.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom # Set AcceptButton for the form $form.AcceptButton = $okButton # Global variable to store the result $global:SelectedOU = $null # Add controls to the form $form.Controls.Add($treeView) $form.Controls.Add($okButton) # Show the form and check the result $result = $form.ShowDialog() if ($result -eq [System.Windows.Forms.DialogResult]::OK) { if ($treeView.SelectedNode -ne $null) { $global:SelectedOU = $treeView.SelectedNode.Tag Write-Host "Selected OU: $global:SelectedOU" } else { [System.Windows.Forms.MessageBox]::Show("No OU was selected.", "Selection Missing", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning) $global:SelectedOU = $null # Ensure it's null if nothing selected } } else { Write-Host "User cancelled the selection." $global:SelectedOU = $null # Ensure it's null if cancelled } # Dispose the form object $form.Dispose() # --- Report Generation Part (Only if an OU was selected) --- if (-not [string]::IsNullOrWhiteSpace($global:SelectedOU)) { Write-Host "Proceeding with report generation for: $global:SelectedOU" # Extract the specific OU name (leaf OU) for the report title/filename $OUName = ($global:SelectedOU -split ',')[0] -replace 'OU=', '' # Sanitize OU Name for use in filename (replace invalid chars) $safeOUName = $OUName -replace '[\\/:*?"<>|]', '_' # Define the path for the HTML report in Downloads folder $DownloadsPath = [System.IO.Path]::Combine($([Environment]::GetFolderPath('UserProfile')), 'Downloads') if (-not (Test-Path $DownloadsPath)) { $DownloadsPath = [Environment]::GetFolderPath('MyDocuments') # Fallback to MyDocuments } $HtmlReportPath = Join-Path -Path $DownloadsPath -ChildPath "OUUsersReport_$safeOUName.html" Write-Host "Retrieving users from '$global:SelectedOU' and its sub-OUs..." # Retrieve AD users - Get-ADUser defaults to -SearchScope Subtree, which is usually desired here # Exclude disabled accounts and those without a UPN (often service/system accounts) try { $Users = Get-ADUser -Filter * -SearchBase $global:SelectedOU -Properties DisplayName, EmailAddress, Title, Department, ` StreetAddress, City, State, PostalCode, Country, Description, Office, Enabled, UserPrincipalName, Name -ErrorAction Stop | Where-Object { $_.Enabled -eq $true -and -not [string]::IsNullOrWhiteSpace($_.UserPrincipalName) } | Sort-Object Name # Sort users by Name Write-Host "Found $($Users.Count) enabled users with UPN." # Generate the HTML report $HtmlReport = @"
Organizational Unit: $OUName ($global:SelectedOU)
Generated on: $(Get-Date)
User Count: $($Users.Count)
| Name (sAMAccountName) | Display Name | Email Address | User Principal Name | Description | Office | Title | Department | Street Address | City | State/Province | Postal Code | Country/Region | Enabled |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| $(Escape-Html $User.Name) | $(Escape-Html $User.DisplayName) | $(Escape-Html $User.EmailAddress) | $(Escape-Html $User.UserPrincipalName) | $(Escape-Html $User.Description) | $(Escape-Html $User.Office) | $(Escape-Html $User.Title) | $(Escape-Html $User.Department) | $(Escape-Html $User.StreetAddress) | $(Escape-Html $User.City) | $(Escape-Html $User.State) | $(Escape-Html $User.PostalCode) | $(Escape-Html $User.Country) | $($User.Enabled) |
| No enabled users found in this OU. | |||||||||||||