<# Alan Kaplan, www.akaplan.com This GUI script lets you export users with any selected attributes. Not required: admin rights or ActiveDirectory module v 2.7 12/24/21 public version #> Add-Type -assemblyname Microsoft.visualBasic #This is used to ensure that input box is modal #http://stackoverflow.com/questions/9978727/focus-window-created-by-powershell-script $activateWindow = { $isWindowFound = $false while (-not $isWindowFound) { try { [microsoft.visualbasic.interaction]::AppActivate($args[0]) $isWindowFound = $true } catch { Start-Sleep -Milliseconds 200 } } } $desktop = [environment]::GetFolderPath('Desktop') $UACcode = @' using System; /// /// Flags that control the behavior of the user account. /// [Flags()] public enum UserAccountControl : int { /// The logon script is executed. SCRIPT = 0x00000001, /// /// The user account is disabled. DISABLED = 0x00000002, /// /// The home directory is required. HOMEDIR_REQUIRED = 0x00000008, /// The account is currently locked out. LOCKEDOUT = 0x00000010, /// No password is required. PASSWD_NOTREQD = 0x00000020, /// The user cannot change the password. /// /// Note: You cannot assign the permission settings of PASSWD_CANT_CHANGE by directly modifying the UserAccountControl attribute. /// For more information and a code example that shows how to prevent a user from changing the password, see User Cannot Change Password. // PASSWD_CANT_CHANGE = 0x00000040, /// The user can send an encrypted password. ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080, /// /// This is an account for users whose primary account is in another domain. This account provides user access to this domain, but not /// to any domain that trusts this domain. Also known as a local user account. TEMP_DUPLICATE_ACCOUNT = 0x00000100, /// /// This is a default account type that represents a typical user. NORMAL_ACCOUNT = 0x00000200, /// This is a permit to trust account for a system domain that trusts other domains. INTERDOMAIN_TRUST_ACCOUNT = 0x00000800, /// This is a computer account for a computer that is a member of this domain. WORKSTATION_TRUST_ACCOUNT = 0x00001000, /// This is a computer account for a system backup domain controller that is a member of this domain. SERVER_TRUST_ACCOUNT = 0x00002000, /// Not used. Unused1 = 0x00004000, /// Not used. Unused2 = 0x00008000, /// The password for this account will never expire. DONT_EXPIRE_PASSWD = 0x00010000, /// This is an MNS logon account. MNS_LOGON_ACCOUNT = 0x00020000, /// The user must log on using a smart card. SMARTCARD_REQUIRED = 0x00040000, /// /// The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service /// can impersonate a client requesting the service. /// TRUSTED_FOR_DELEGATION = 0x00080000, /// /// The security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation. /// NOT_DELEGATED = 0x00100000, /// Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. USE_DES_KEY_ONLY = 0x00200000, /// This account does not require Kerberos pre-authentication for logon. DONT_REQUIRE_PREAUTH = 0x00400000, /// /// The user password has expired. This flag is created by the system using data from the Pwd-Last-Set attribute and the domain policy. /// PWD_EXPIRED = 0x00800000, /// /// The account is enabled for delegation. This is a security-sensitive setting; accounts with this option enabled should be strictly /// controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to /// other remote servers on the network. /// TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000, /// Partial Secrets Account PARTIAL_SECRETS_ACCOUNT = 0x04000000, /// AES Keys required USE_AES_KEYS = 0x08000000 } '@ Add-Type $UACcode #Hash table to store group Info $script:ADGroupCache = @{ } #Hash table to store Manager Info $ManagerHT = @{} Enum eType { DES_CBC_CRC = 0x01 DES_CBC_MD5 = 0x02 RC4_HMAC = 0x04 AES128_CTS_HMAC_SHA1_96 = 0x08 AES256_CTS_HMAC_SHA1_96 = 0x10 } Function Get-SupportedEncryption ($UserEncType) { switch ($UserEncType) { $null { #default when null is RC4 '[Empty] RC4_HMAC_MD5' } 0 { '[0] RC4_HMAC_MD5' } Default { $encList = [System.Collections.Generic.List[string]]::new() [enum]::getvalues([eType]) | ForEach-Object { if (($UserEncType -band [eType]::$_) -eq [eType]::$_) { $EncList.add($_) } } "[$UserEncType] " + ($encList -join (', ')).tostring() } } } Function Get-SaveFile($Prompt, $filter, $initialDirectory, [string]$defaultFile) { add-type -assemblyname System.Windows.Forms $SaveFileDialog = New-Object System.Windows.Forms.SaveFileDialog $SaveFileDialog.initialDirectory = $initialDirectory $SaveFileDialog.filter = $filter $SaveFileDialog.Title = $Prompt $SaveFileDialog.filename = $defaultFile if ($saveFileDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $SaveFileDialog.filename } ELSE {} } #Convert ADSPath/DistinguishedName to Canonical path using strings only Function Convert-DNtoCanonical($adspath) { #Clean anything to left of CN= $Cleaned = ([regex]::Replace($adspath, '^.*CN=', '')) -replace '\\', '' $a = $Cleaned.split(',') #$sCN = $a[0] $sCN = $cleaned.substring(0, $cleaned.indexof('=') - 3) $aMid = ($a | Where-Object { $_.Startswith('OU=') }) -replace ('OU=') [array]::Reverse($aMid) $sMid = $aMid -join '/' $sDomain = ($a | Where-Object { $_.Startswith('DC=') }) -replace ('DC=') -join '.' ($sDomain, $sMid, $sCN) -join '/' } #Bill Stewart function Invoke-Method([__ComObject] $object, [String] $method, $parameters) { $object.GetType().InvokeMember($method, "InvokeMethod", $NULL, $object, $parameters) } Function Get-GroupNTName($GroupDN) { if ($ADGroupCache.ContainsKey($GroupDN)) { Write-Verbose "Found group in cache: $GroupDN" $groupNT = $ADGroupCache[$GroupDN] } Else { $groupNT = Convert-DNtoNTName $GroupDN $ADGroupCache.Add($GroupDN, $groupNT) } $GroupNT } Function Get-GroupNames($ArrMembers) { if (($ArrMembers).length -eq 0) { $strGrpList = '' } Else { $aGrpList = ForEach ($groupDN in $ArrMembers) { ([string]$(Get-GroupNTName $groupDN)).trim() } $strGrpList = $aGrpList -join ('; ') } $strGrpList } function Get-ManagerUPN($managerDN) { if ($ManagerDN) { if ($ManagerHT.ContainsKey($managerDN)) { $mgrUPN = $ManagerHT[$ManagerDN] } Else { $ManagerDom = Get-DomainFromDNString $managerDN Try { $oManager = [ADSI]"LDAP://$ManagerDom/$managerDN" $mgrUPN = $oManager.properties['userPrincipalName'].value $ManagerHT.Add($managerDN, $mgrUPN) } Catch {} } $mgrUPN } } #This function converts a DN to a NT Name, removing the odd spaces sometimes appearing with nametranslate Function Convert-DNtoNTName($strDN) { $NameTranslate = New-Object -ComObject "NameTranslate" Invoke-Method $NameTranslate "Init" (3, "") Invoke-Method $NameTranslate "Set" (1, $strDN) $retval = (Invoke-Method $NameTranslate "Get" (3)) $retval.ToString().trim() [void][System.Runtime.Interopservices.Marshal]::ReleaseComObject($NameTranslate) } Function Get-DomainFromDNString($strADsPath) { if ($strADsPath.startsWith('DC=')) { ($strADsPath.Replace("DC=", ".").Replace(',', '')).Substring(1) } Else { $strADsPath.Substring($strADsPath.IndexOf(",DC")).Replace(",DC=", ".").Substring(1) } } $i = 0 Add-Type -Assembly PresentationFramework #Get members of group with selected properties Function Select-PropertiesForm { Param ( # ObjectProps help description [Parameter(Mandatory = $True, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [Object[]]$objectProps, # FormTitle help description [Parameter(Mandatory = $False, Position = 1)] [string]$FormTitle = "Select in order the properties to report, then accept list", # When True, Exit if cancel or close without selecting any properties. Default is continue [Parameter(Mandatory = $False, Position = 2)] [Switch]$ExitOnCancel = $False ) #Initial form code generated by SAPIEN Technologies PrimalForms (Community Edition) v1.0.10.0 Add-Type -assemblyname System.Windows.Forms Add-Type -assemblyname System.Drawing $script:NewList = "" $SelectForm = New-Object System.Windows.Forms.Form $CancelBtn = New-Object System.Windows.Forms.Button $AcceptBtn = New-Object System.Windows.Forms.Button $MoveLftBtn = New-Object System.Windows.Forms.Button $MoveRtBtn = New-Object System.Windows.Forms.Button $lb_NewList = New-Object System.Windows.Forms.ListBox $lb_OrigList = New-Object System.Windows.Forms.ListBox $lb_OrigList.Sorted = $true $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState #Event Script Blocks $On_AcceptBtn_Click = { $script:NewList = @() foreach ($item in $lb_NewList.Items) { $script:NewList += $item } $SelectForm.Close() } $On_CancelBtn_Click = { $SelectForm.Close() } # Begin by loading data passed to function $On_SelectForm_Load = { foreach ($item in $objectProps) { $lb_OrigList.items.add($item) } } #Click the move left button removes from New list and puts back item to the original list $On_MoveLftBtn_Click = { $movelist = @() foreach ($item in $lb_NewList.SelectedItems) { $movelist += $item } foreach ($item in $movelist) { $lb_NewList.items.remove($item) $lb_OrigList.items.add($item) } } #Click the move right button removes from original list and puts item to the new list $On_MoveRtBtn_Click = { $movelist = @() foreach ($item in $lb_OrigList.SelectedItems) { $movelist += $item } foreach ($item in $movelist) { $lb_OrigList.items.remove($item) $lb_NewList.items.add($item) } } $OnLoadForm_StateCorrection = { #Sapien says this corrects the initial state of the form to prevent the .Net maximized form issue $SelectForm.WindowState = $InitialFormWindowState } $SelectForm.CancelButton = $CancelBtn $SelectForm.AutoSize = $True $SelectForm.StartPosition = "CenterScreen" $SelectForm.Name = "SelectForm" $SelectForm.Text = $FormTitle $SelectForm.add_Load($On_SelectForm_Load) #Cancel Button $CancelBtn.DialogResult = 2 $CancelBtn.Location = New-Object System.Drawing.Point 255, 220 $CancelBtn.Name = "CancelBtn" $CancelBtn.Size = New-Object System.Drawing.Size 75, 25 $CancelBtn.TabIndex = 5 $CancelBtn.Text = "Cancel" $CancelBtn.UseVisualStyleBackColor = $True $CancelBtn.add_Click($On_CancelBtn_Click) $SelectForm.Controls.Add($CancelBtn) #Accept Button $AcceptBtn.Location = New-Object System.Drawing.Point 255, 175 $AcceptBtn.Name = "AcceptBtn" $AcceptBtn.Size = New-Object System.Drawing.Size 75, 25 $AcceptBtn.TabIndex = 4 $AcceptBtn.Text = "Accept List" $AcceptBtn.UseVisualStyleBackColor = $True $AcceptBtn.add_Click($On_AcceptBtn_Click) $SelectForm.Controls.Add($AcceptBtn) #Move Left Button $MoveLftBtn.Location = New-Object System.Drawing.Point 255, 110 $MoveLftBtn.Name = "MoveLftBtn" $MoveLftBtn.Size = New-Object System.Drawing.Size 75, 45 $MoveLftBtn.TabIndex = 3 $MoveLftBtn.Text = "<< Move" $MoveLftBtn.UseVisualStyleBackColor = $True $MoveLftBtn.add_Click($On_MoveLftBtn_Click) $SelectForm.Controls.Add($MoveLftBtn) #Move Right Button $MoveRtBtn.Enabled = $True $MoveRtBtn.Location = New-Object System.Drawing.Point 255, 40 $MoveRtBtn.Name = "MoveRtBtn" $MoveRtBtn.Size = New-Object System.Drawing.Size 75, 45 $MoveRtBtn.TabIndex = 2 $MoveRtBtn.Text = "Move >>" $MoveRtBtn.UseVisualStyleBackColor = $True $MoveRtBtn.add_Click($On_MoveRtBtn_Click) $SelectForm.Controls.Add($MoveRtBtn) #Left Panel original data $lb_OrigList.FormattingEnabled = $True $lb_OrigList.Location = New-Object System.Drawing.Point 10, 5 $lb_OrigList.Name = "listBox1" $lb_OrigList.Size = New-Object System.Drawing.Size 235, 355 $lb_OrigList.TabIndex = 0 $lb_origList.SelectionMode = "multiExtended" $SelectForm.Controls.Add($lb_OrigList) #Right Panel new data $lb_NewList.FormattingEnabled = $True $lb_NewList.Location = New-Object System.Drawing.Point 345, 5 $lb_NewList.Name = "listBox2" $lb_NewList.Size = New-Object System.Drawing.Size 235, 355 $lb_NewList.TabIndex = 1 $lb_NewList.SelectionMode = "multiExtended" $SelectForm.Controls.Add($lb_NewList) #Sapien says save the initial state of the form $InitialFormWindowState = $SelectForm.WindowState #Init the OnLoad event to correct the initial state of the form $SelectForm.add_Load($OnLoadForm_StateCorrection) #Show the Form $SelectForm.ShowDialog() | Out-Null #If they choose nothing or close, return all if ($script:NewList[0].Count -eq 0) { If ($ExitOnCancel) { Break }ELSE { $objectProps } } ELSE { $script:NewList } } Function ConvertTo-PSObjectFromDirectoryEntry { Param ( # oDS is a directory services object [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [ValidateNotNullOrEmpty()] $oDS, # Optional list of parameters [Parameter(Mandatory = $False, ValueFromPipeline = $False, Position = 1)] $PropList ) $outval = New-Object -TypeName PsObject if ($null -eq $PropList) { $PropList = $oDS.Properties.PropertyNames } foreach ($strProperty in $PropList) { Write-verbose $strProperty Try { $val = $oDS.Properties[[string]$strProperty].Value $basename = $val.gettype().basetype.name switch ($basename) { 'Array' { $val = $val -join ', ' ; break } 'Object' { if ($val.GetTypeCode() -eq 'byte') { $val = ByteToString ($val) } ; break } 'MarshalByRefObject' { if (($val -eq 0) -or ($null -eq $val) ) { $val = '' Break } Try { $val = [datetime]::fromfiletime($oDS.ConvertLargeIntegerToInt64($val)) if ([datetime]::Parse($val).year -eq 1600) { $val = '' } } Catch { $val = '' } break } Default { } } } Catch { $val = '' } Add-Member -InputObject $outval -MemberType NoteProperty ` -Name $strProperty -Value $val -force } $outval } Function ByteToString($v) { $ADPropVal = '' #Delim character is used to join string of bytes $delim = ";" if ($v.count -eq 1) { $v.tostring() } Else { For ($i = 0; $i -le $v.count - 1; $i++) { if ($v.item($i)) { $ADPropVal += $v.item($i).ToString() + $delim } } } #Cleanup to remove trailing delimter $adpropval.TrimEnd($delim) } Function Get-SaveFile($Prompt, $filter, $initialDirectory, [string]$defaultFile) { add-type -assemblyname System.Windows.Forms $SaveFileDialog = New-Object System.Windows.Forms.SaveFileDialog $SaveFileDialog.initialDirectory = $initialDirectory $SaveFileDialog.filter = $filter $SaveFileDialog.Title = $Prompt $SaveFileDialog.filename = $defaultFile if ($saveFileDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $SaveFileDialog.filename } ELSE { } } Function Select-ADOU { [CmdletBinding()] Param ( # Title text [Parameter(Position = 0)] $TitleText, # Instruction text is below title, and above domain box [Parameter(Position = 1)] [string]$InstructionText, # Select Button Text [Parameter(Position = 3)] [string]$BtnText, # Show Containers. Default is only OUs [Parameter(Mandatory = $False)] [Switch]$ShowContainers = $False, # Force Single Domain. No navigation within forest [Parameter(Mandatory = $False)] [Switch]$SingleDomain = $False, #Set the initial domain. This must be a FQDN, example Contoso.com [Parameter(Mandatory = $False)] [string]$InitialDomain, #Set the StartingOU. This must be a dn, example 'OU=workstations,DC=Contoso,DC=com' [Parameter(Mandatory = $False)] [string]$StartOUdN, #Use Checkboxes [Parameter(Mandatory = $False)] [switch]$ShowCheckBoxes = $False ) ### Helper functions ### Function Show-NoCheck { #Show the Form without CheckBoxes $dialogResult = $Form.ShowDialog() if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK) { If ($null -ne $treeViewNav.SelectedNode) { $OUName = [string]$treeViewNav.SelectedNode.Text $OUDN = [string]$treeViewNav.SelectedNode.Tag } ELSE { $OUName = [string]$treeViewNav.Nodes[0].Text $OUDN = [string]$treeViewNav.Nodes[0].Tag } $RetVal = [PSCustomObject]@{ Domain = [string]$DomainBox.SelectedItem OUName = $OUName OUDN = $OUDN } $RetVal $Form.Close() } } #Sapien.com, see notes Function Get-CheckedNodes { param( [ValidateNotNull()] [System.Windows.Forms.TreeNodeCollection] $NodeCollection, [ValidateNotNull()] #[System.Collections.Generic.List][object]$CheckedNodes [System.Collections.ArrayList]$CheckedNodes ) foreach ($Node in $NodeCollection) { if ($Node.Checked) { [void]$CheckedNodes.Add($Node) } Get-CheckedNodes $Node.Nodes $CheckedNodes } } Function Show-CheckBoxes { #Show the Form $dialogResult = $Form.ShowDialog() if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK) { $CheckedNodes = New-Object System.Collections.ArrayList Get-CheckedNodes $treeViewNav.Nodes $CheckedNodes if ($CheckedNodes.count -gt 0) { $RetVal = foreach ($node in $CheckedNodes) { [PSCustomObject]@{ Domain = [string]$DomainBox.SelectedItem OUName = $node.Text OUDN = [string]$Node.Tag } } $RetVal } ELSE { [PSCustomObject]@{ Domain = [string]$DomainBox.SelectedItem OUName = [string]$treeViewNav.Nodes[0].Text OUDN = [string]$treeViewNav.Nodes[0].Tag } } $Form.Close() } } Function Test-DomainExists ($dom) { Try { [adsi]::Exists("LDAP://$dom") } Catch { $False } } $Script:LoadCount = 0 ### End helper functions### #Default display text if not set by argument if ($TitleText.length -eq 0) { if ($ShowCheckBoxes) { $TitleText = "Select OU(s)" } ELSE { $TitleText = "Select an OU" } } if ($BtnText.length -eq 0) { if ($ShowCheckBoxes) { $BtnText = "Accept Selected OU(s)" } ELSE { $BtnText = "Accept Selected OU" } } if ($InstructionText.length -eq 0) { if ($SingleDomain) { $InstructionText = "Double click on a node to expand." } ELSE { $InstructionText = "Selecting new domain will show base level. Double click on a node to expand." } } if ($ShowContainers -eq $True) { $LDAPFilter = '(|(objectClass=container)(ObjectCategory=OrganizationalUnit))' } ELSE { $LDAPFilter = '(ObjectCategory=OrganizationalUnit)' } if ($InitialDomain.Length -eq 0) { $InitialDomain = ([System.DirectoryServices.ActiveDirectory.domain]::GetCurrentDomain()).Name } If ($SingleDomain) { $DomainList = $InitialDomain } ELSE { $Forest = ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()) $DomainList = ($Forest.Domains).name | Sort-Object -CaseSensitive } $startNum = $DomainList.IndexOf($InitialDomain) # Import the Assemblies Add-Type -assemblyname System.Windows.Forms Add-Type -assemblyname System.Drawing Add-Type -assemblyname Microsoft.VisualBasic # Form Objects $Form = New-Object System.Windows.Forms.Form $DomainBox = New-Object System.Windows.Forms.ListBox $DomainLbl = New-Object System.Windows.Forms.Label $AcceptBtn = New-Object System.Windows.Forms.Button $treeViewNav = New-Object System.Windows.Forms.TreeView $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState $StatusBar = New-Object System.Windows.Forms.StatusBar $InstructionsLabel = New-Object System.Windows.Forms.Label #Event Script Blocks $DomainBox_SelectedIndexChanged = { $CurrentDomain = $DomainBox.SelectedItem $DomainBox.topindex = $DomainBox.SelectedIndex - 1 #check communication with selected domain if ((Test-DomainExists $CurrentDomain) -eq $False) { Write-Warning "$CurrentDomain does not exist or cannot be contacted." Exit } $domain = [adsi]"LDAP://$CurrentDomain" if ($Script:LoadCount -gt 0) { $StartOUdN = '' $InstructionsLabel.Text = $instructionText } Else { $InstructionsLabel.Text = $instructionText } $Script:LoadCount ++ #Clear old results $TreeviewNav.Nodes.Clear() $newnode = New-Object System.Windows.Forms.TreeNode $newnode.Name = $domain.Name if (!$StartOUdN) { $StartOUdN = $domain.distinguishedName } $newnode.Text = $StartOUdN $newnode.Tag = $StartOUdN $treeviewNav.Nodes.Add($newnode) #Expand the initial tree Invoke-command $treeviewNav_DoubleClick } $treeviewNav_DoubleClick = { [System.Windows.Forms.Application]::UseWaitCursor = $true $StatusBar.Text = "Getting list, please wait..." if ($null -eq $treeviewNav.SelectedNode) { #For first listing in treeview, select root $node = $treeviewNav.Nodes[0] } Else { $node = $treeviewNav.SelectedNode } if ($node.Nodes.Count -eq 0) { $SearchRoot = "LDAP://$($node.Tag)" $ADSearcher = [adsisearcher] '' $ADSearcher.SearchRoot = $SearchRoot $ADSearcher.PageSize = 500 $ADSearcher.SearchScope = "OneLevel" $ADSearcher.CacheResults = $false $ADSearcher.Filter = $LDAPFilter $List = ($ADSearcher.FindAll()).getEnumerator().properties foreach ($OU in $List) { $OUName = $OU.Item("Name") $OUDN = $OU.Item("DistinguishedName") $newnode = New-Object System.Windows.Forms.TreeNode $newnode.Name = $OUName $newnode.Text = $OUName $newnode.Tag = $OUDN $node.Nodes.Add($newnode) } } $node.Expand() [System.Windows.Forms.Application]::UseWaitCursor = $False $statusbar.text = "" } $OnLoadForm_StateCorrection = { #Correct the initial state of the form to prevent the .Net maximized form issue $Form.WindowState = $InitialFormWindowState } $form.ClientSize = New-Object System.Drawing.Size(445, 595) $Form.Name = "Form" $Form.Text = $TitleText $InstructionsLabel.Location = New-Object System.Drawing.Point(10, 15) $InstructionsLabel.Name = "InstructionsLabel" $InstructionsLabel.Size = New-Object System.Drawing.Size(420, 35) $InstructionsLabel.Font = New-Object System.Drawing.Font("Microsoft Sans Serif", 8.25, 2, 3, 0) $InstructionsLabel.Text = $InstructionText $Form.Controls.Add($InstructionsLabel) $DomainBox.Name = "DomainBox" $DomainBox.FormattingEnabled = $True $DomainBox.Font = New-Object System.Drawing.Font("Microsoft Sans Serif", 9.75, 0, 3, 0) $DomainBox.Location = New-Object System.Drawing.Point(185, 50) $DomainBox.Size = New-Object System.Drawing.Size(160, 55) $DomainBox.add_SelectedIndexChanged($DomainBox_SelectedIndexChanged) foreach ($Domain in $domainlist) { [void] $DomainBox.Items.Add($domain) } $DomainBox.setSelected($startNum, $true) $Form.Controls.Add($DomainBox) $DomainLbl.Name = "DomainLbl" $DomainLbl.Location = New-Object System.Drawing.Point(20, 64) $DomainLbl.Size = New-Object System.Drawing.Size(160, 25) $DomainLbl.Text = "Selected Domain" $DomainLbl.TextAlign = "Middleright" $Form.Controls.Add($DomainLbl) $AcceptBtn.Name = "AcceptBtn" $AcceptBtn.Location = New-Object System.Drawing.Point(125, 555) $AcceptBtn.Size = New-Object System.Drawing.Size(170, 25) $AcceptBtn.Text = $BtnText $AcceptBtn.DialogResult = [System.Windows.Forms.DialogResult]::OK $Form.Controls.Add($AcceptBtn) $form.AcceptButton = $AcceptBtn $treeViewNav.Name = "treeViewNav" $treeViewNav.CheckBoxes = $ShowCheckBoxes $treeViewNav.Location = New-Object System.Drawing.Point(45, 110) $treeViewNav.Size = New-Object System.Drawing.Size(325, 440) $treeViewNav.add_DoubleClick($treeViewNav_DoubleClick) $treeViewNav.add_AfterSelect($treeViewNav_AfterSelect) $Form.Controls.Add($treeViewNav) $form.StartPosition = "CenterParent" $form.Controls.Add($StatusBar) #Save the initial state of the form $InitialFormWindowState = $Form.WindowState #Init the OnLoad event to correct the initial state of the form $Form.add_Load($OnLoadForm_StateCorrection) if ($ShowCheckBoxes) { Show-CheckBoxes }ELSE { Show-NoCheck } } #End Function Function Get-CustomReport { $params = @{ BtnText = "Continue" ShowCheckBoxes = $false InstructionText = "Double click on an OU to expand. Users in and below selected OU will be exported." } $ADretval = Select-ADOU @params -InitialDomain $domain #Extract site code from OU DN if available $OUDN = $ADretval.OUDN $strRegex = '\((.*?)\)' if ([regex]::IsMatch($OUDN, $strRegex)) { $site = [regex]::Matches($OUDN, $strRegex).groups[1].value $site += '_' } Else { $site = '' } if ($null -eq $ADretval) { Stop-Process $PID } $ReportFile = "$Desktop\$site`UserExport_" + $(Get-Date).ToString("yyyyMMdd_HHmm") + '.csv' #Splatting Get-SaveFile parameters $params = @{ Prompt = "Select name and path for CSV report file" Filter = "CSV Files (*.csv)|*.csv" InitialDirectory = $desktop DefaultFile = $reportfile } $reportfile = Get-SaveFile @params if ([System.String]::IsNullOrEmpty($ReportFile)) { Exit } Write-Host "Getting available properties from schema, please wait ...." -ForegroundColor Green $schema = [DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema() $userClass = $schema.FindClass('user') $UserProps = $userClass.mandatoryProperties.name + $userClass.OptionalProperties.name + 'domain,enabled,smartCardLogonRequired'.Split(',') | where-object { $_ -notmatch 'logonhours' } | Sort-Object Clear-Host #message for instructions menu`"Domain`", $msg = @" This script exports users with selected properties. The properties are collected from the AD Schema, and some are populated. `"Enabled`" and `"SmartCardLogonRequired`" are available without selecting UserAccountControl. The domain and canonicalname of an object are available, and dates are converted when appropriate. If you select `"memberOf`", the script will return the user's groups in the NT format. Note these less intuitive names: `"l`" is City, and `"info`" is the AD notes field. A Google search of `"ldap common user properties`" gives good results for attribute names. Nested groups add the object class and parent group to the results.`n The next screen lets you select the attributes you want in the results. Keyboard search is first letter only. Attributes should be selected in order, using `"Move`" to choose, then `"Accept List`" to begin the export. "@ #if you want line above without line break $title = "Instructions" #Ensure Inputbox is top with these three lines $job = Start-Job $activateWindow -ArgumentList $title [void][Microsoft.VisualBasic.Interaction]::MsgBox($msg, 'DefaultButton1, OkOnly, Information', $title) Remove-Job $job -Force [array]$RequestedPropList = Select-PropertiesForm $UserProps -FormTitle "Select user properties in order desired in output." -ExitOnCancel if (($RequestedPropList.Count -ge 200) -or ($RequestedPropList.Count -eq 0)) { Exit } $PropList = New-Object System.Collections.ArrayList(, ($RequestedPropList)) $SortBy = Select-PropertiesForm -FormTitle "Sort order for report. Select one or more property" -objectProps $RequestedPropList if ($null -eq $SortBy) { Exit } if ([regex]::IsMatch( $RequestedPropList, 'enabled|SmartCardLogonRequired')) { if ($RequestedPropList -notcontains 'useraccountcontrol') { [void]$PropList.Add('userAccountControl') } [regex]::Matches( $RequestedPropList, 'enabled|SmartCardLogonRequired') | ForEach-Object { [void]$PropList.Remove($_.value) } } if ($RequestedPropList -notcontains 'Name') { [void]$PropList.Add('Name') } if ($RequestedPropList -notcontains 'Class') { [void]$PropList.Add('Class') } if ($RequestedPropList -Match 'domain|canonicalName') { if ($RequestedPropList -notcontains 'distinguishedName') { [void]$PropList.Add('distinguishedName') } [regex]::Matches( $RequestedPropList, 'domain|canonicalName') | ForEach-Object { [void]$PropList.Remove($_.value) } } #Export users with property list foreach ($ADSPath in $ADretval) { Clear-Host $domain = $ADSPath.Domain $OU = $ADSPath.OUDN Get-UserList -domain $domain -OU $OU -Properties $proplist } } Function ToTitleCase($txt) { $TextInfo = (Get-Culture).TextInfo $TextInfo.ToTitleCase($txt.toLower()) } Function Get-CertInfo ($certs) { $Certs | ForEach-Object { $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $cert.Import([byte[]]$_) $Issuer = $cert.Issuer $IssuerName = $Issuer.Substring(3, $Issuer.IndexOf(", OU=") - 3) $subject = $cert.Subject $SName = ([regex]::Split($subject, 'CN=|\+|OID|,')[1]).trim() $subject = ToTitleCase -txt $SName $keyUsage = $cert.extensions.keyusages if ($cert.EnhancedKeyUsageList.count -gt 0) { $EnhancedUsage = ($cert.EnhancedKeyUsageList).friendlyname -join ", " $keyUsage = "$keyUsage`: $enhancedusage" } $certInfo = [PsCustomObject]@{ IssuedTo = $subject Expires = $cert.NotAfter SN = $cert.SerialNumber Issuer = $IssuerName Usage = $keyUsage } if ($(Get-Date) -gt $cert.NotAfter ) { $Expired = 'True' }Else { $Expired = 'False' } $CertInfo | Add-Member -NotePropertyName Expired -NotePropertyValue $Expired -PassThru } } #https://petri.com/expanding-active-directory-searcher-powershell Function Convert-ADSearchResult { [cmdletbinding()] Param( [Parameter(Position = 0, Mandatory, ValueFromPipeline)] [ValidateNotNullorEmpty()] $SearchResult ) Begin { Write-Verbose "Starting $($MyInvocation.MyCommand)" } Process { Write-Verbose "Processing result for $($searchResult.Path)" #create an ordered hashtable with property names alphabetized $props = $SearchResult.Properties.PropertyNames | Sort-Object $objHash = [ordered]@{} foreach ($p in $props) { $value = $searchresult.Properties.item($p) if ($p -match 'cert') { $certs = ($SearchResult.GetDirectoryEntry()).$p if ($certs.count -gt 0) { $certInfo = Get-CertInfo $certs [string]$CertTxt = @(($certinfo | get-member -MemberType Noteproperty).definition | Foreach-object { [regex]::Replace($_ , '^\w*\s*', "" ) }) -join '; ' $value = $CertTxt } } Else { if ($value.count -eq 1) { $value = $value[0] } } $objHash.add($p, $value) } new-object psobject -property $objHash } End { Write-Verbose "Ending $($MyInvocation.MyCommand)" } } Function Get-UserList ($domain, $OU, $Properties) { Write-Host "Searching $domain for selected user objects, please wait ...." -foregroundColor Green $SearchPath = "LDAP://$domain/$OU" $de = New-Object System.DirectoryServices.DirectoryEntry($SearchPath) $ds = New-Object System.DirectoryServices.DirectorySearcher $ds.SearchRoot = $de $ds.PageSize = 500 $ds.PropertiesToLoad.AddRange(@($Properties)) $ds.Filter = "(objectCategory=user)" $ds.SearchScope = "SubTree" $retval = $ds.FindAll() $iUsercount = $retval.Count if ($iUsercount -gt 0) { $data = $retval.GetEnumerator() | ForEach-Object { $i++ [int]$pct = ($i / $iUserCount) * 100 $ProgParam = @{ Activity = "Getting user information" CurrentOperation = "$i of $iUserCount" Status = "Please wait ..." PercentComplete = $pct } Write-Progress @progParam Convert-ADSearchResult $_ } Write-Progress "Getting additional information (groups, manager, etc.)" $selectProps = New-Object -TypeName System.Collections.ArrayList $DateProps = 'accountExpires,badPasswordTime,lastLogoff,lastLogon,lastLogonTimestamp,lockoutDuration,lockOutObservationWindow,lockoutTime,maxPwdAge,minPwdAge,msDS-LastFailed,InteractiveLogonTime,msDS-LastSuccessfulInteractiveLogonTime,msDS-UserPassword,ExpiryTimeComputed,pwdLastSet'.Split(',') foreach ($field in $RequestedPropList) { switch ($field) { 'domain' { $selectProps.Add(@{ Name = 'Domain' Expression = [Scriptblock]::Create("Get-DomainFromDNString -stradspath (`$_).distinguishedName") }) | out-null break } 'Enabled' { $selectProps.Add(@{ Name = 'Enabled' Expression = [Scriptblock]::Create("![bool]((`$_.userAccountControl -band 2) -eq 2)" ) }) | out-null break } 'SmartCardLogonRequired' { $selectProps.add(@{ Name = 'SmartCardLogonRequired' Expression = [Scriptblock]::Create("[bool]((`$_.userAccountControl -band 262144) -eq 262144)") }) | out-null break } 'canonicalName' { $selectProps.add(@{ Name = 'CanonicalName' Expression = [Scriptblock]::Create("Convert-DNtoCanonical -adspath `$_.distinguishedName") }) | Out-Null break } 'UserAccountControl' { $selectProps.Add(@{ Name = 'UserAccountControl' Expression = [Scriptblock]::Create("'[' + `$_.userAccountControl + '] ' + [UserAccountControl]`$_.userAccountControl") }) | out-null break } 'MsDS-SupportedEncryptionTypes' { $selectProps.Add(@{ Name = 'SupportedEncryptionTypes' Expression = [Scriptblock]::Create("Get-SupportedEncryption `$_.`'MsDS-SupportedEncryptionTypes`'") }) | out-null break } 'MemberOf' { $selectProps.Add(@{ Name = 'Groups' Expression = [Scriptblock]::Create("Get-GroupNames `$_.memberOf") }) | out-null break } 'Manager' { $selectProps.Add(@{ Name = 'ManagerUPN' Expression = [Scriptblock]::Create("Get-ManagerUPN `$_.manager") }) | out-null break } Default { if ($field -in $DateProps) { $selectProps.add(@{ Name = "$field " Expression = [Scriptblock]::Create("[datetime]::FromFileTime(`$_.'$field')") }) | Out-Null } Else { $field = $field.ToString() $selectProps.add($field) | out-null } } } } } ELSE { Write-warning "Search failed for `n$searchroot" Pause } $data | Select-Object -property $SelectProps | Sort-Object -Property $SortBy | Select-Object * -Unique | export-csv $ReportFile -NoTypeInformation -Append Write-Progress -Completed "Done" Write-Host "Done." -foregroundColor Green Invoke-Item $ReportFile } # Script Starts $ForestName = [system.directoryservices.activedirectory.forest]::GetCurrentForest().Name.ToString() #Get my user account for initial search base $SearchRoot = "GC://$ForestName" $filter = "(&(objectCategory=User)(objectClass=Person)(samAccountName=$env:UserName))" $ADSearcher = [adsisearcher] '' $ADSearcher.SearchRoot = $SearchRoot $ADSearcher.PageSize = 500 $ADSearcher.SearchScope = "SubTree" $ADSearcher.CacheResults = $false $ADSearcher.Filter = $filter $domain = ($env:USERDNSDOMAIN).ToString().ToLower() Get-CustomReport