<# Alan Kaplan, www.akaplan.com This GUI script exports group membership with any selected attributes It takes the group's distinguishedname as a parameter, and supports very large groups Not required: admin rights or ActiveDirectory module Public Version 12/25/21 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String]$GroupdN, #Get group members recursively [Parameter()] [Switch]$GetNested ) Add-Type -AssemblyName Microsoft.visualBasic Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing Add-Type -AssemblyName PresentationFramework $script:bFirstRun = $true #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 } } } function Get-LargeGroupMembers { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $groupDN, [switch] $showProgress ) begin { $script:ScriptStart = Get-Date Function AddMember($item) { [void]$members.Add($psitem) $memCount = ($members).count if (($memCount % 1500) -eq 0) { if ($showProgress) { Write-Progress "Found $memCount group members" } } } #Basic escape $GroupDN = ($groupDN).replace('/', '\/') #Test if group object exists if (([adsi]::Exists("LDAP://$GroupDN")) -eq $false) { Write-Warning "Failed to find $groupDN" Exit } #Bind to group to get SamAccountName $oGroup = [ADSI]"LDAP://$GroupDN" $script:GroupSAM = [string]$ogroup.Properties['SamAccountName'].value #get AD forest, use Global catalog for search $searchRoot = [system.directoryservices.activedirectory.forest]::GetCurrentForest().Name.ToString() $SearchPath = "GC://" + $SearchRoot $de = New-Object System.DirectoryServices.DirectoryEntry($SearchPath) $ds = New-Object System.DirectoryServices.DirectorySearcher $ds.SearchRoot = $de #Query using ObjectCategory which is indexed. ObjectClass is not $ds.Filter = "(&(objectCategory=group)(sAMAccountName=$GroupSAM))" $Properties = "cn,distinguishedName,member".Split(",") $ds.PropertiesToLoad.AddRange(@($Properties)) $results = $ds.FindOne() #User Arraylist for results for speed $members = New-Object System.Collections.ArrayList } process { if ($pageProperty = $results.Properties.PropertyNames.Where( { $psitem -match '^member;range' }) -as [String]) { #if member;range is in result, use it then get more write-verbose ($pageproperty.split(';'))[1] $directoryEntry = $results.Properties.adspath -as [String] $increment = $results.Properties.$pageProperty.count -as [Int] $results.Properties.$pageProperty.Foreach( { addMember $psitem }) $end = $increment do { $start = $end $end = $start + $increment $memberProperty = 'member;range={0}-{1}' -f $start, $end #New query for additional ranges $memberPager = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList $directoryEntry, '(objectClass=*)', $memberProperty, 'Base' $pageResults = $memberPager.FindOne() $pageProperty = $pageResults.Properties.PropertyNames.Where( { $psitem -match '^member[;:]range' }) -as [String] $pageResults.Properties.$pageProperty.Foreach( { addMember $psitem }) write-verbose ($pageproperty.split(';'))[1] } until ( $pageProperty -match '^member.*\*$' ) } else { $results.Properties.member.Foreach( { addMember $psitem }) } } end { if ($showProgress) { Write-Progress "Done" -Completed } write-verbose "Variable `$members has $($members.count) members" $members.Sort() $members } } $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 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() } } } #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 '/' } 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 } } 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 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 $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 = { #lazy Exit } # 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 if ($strProperty -match 'cert') { $certs = $oDS.$strProperty if ($certs.count -gt 0) { $certInfo = Get-CertInfo $certs [string]$CertTxt = @(($certinfo | get-member -MemberType Noteproperty).definition | Foreach-object { [regex]::Replace($_ , '^\w*\s*', "" ) }) -join '; ' $val = $CertTxt } Else { $val = '' } } Else { 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) { $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 Get-DomainGroupMember { [cmdletbinding()] Param ( [parameter()] $DomainGroup ) $gDn = $DomainGroup.distinguishedName $domainGroupName = $DomainGroup.name Try { $iMemberCount = $DomainGroup.member.count if ($iMemberCount -lt 1500) { $DomainGroupMembers = $DomainGroup.Member } Else { $DomainGroupMembers = Get-LargeGroupMembers -groupdn $DomainGroup.distinguishedName -showProgress $iMemberCount = $DomainGroupMembers.count if ($script:bFirstRun = $true) { #Ensure msgbox is top with these three lines $title = "Continue?" $msg = "$Groupname has $iMemberCount members. Are you sure you want to get user information for this many group members?" $job = Start-Job $activateWindow -ArgumentList $title $retval = [Microsoft.VisualBasic.Interaction]::MsgBox($msg, 'DefaultButton2, YesNo, Question', $title) Remove-Job $job -Force if ($retval -eq 'No') { Exit } } } ForEach ($MemberDN In $DomainGroupMembers) { $objMember = [ADSI]("LDAP://{0}" -f ($MemberDN -replace '/', '\/')) #Create separate object for reporting $objMember | ConvertTo-PSObjectFromDirectoryEntry -PropList $proplist | set-variable oMember Add-Member -inputObject $oMember -NotePropertyName 'Type' -NotePropertyValue $objMember.class -Force if ($GetNested) { $pName = $domaingroup.name[0] Add-Member -inputobject $oMember -NotePropertyName 'ParentGroup' -NotePropertyValue $pname -force } #put reporting object into pipeline $oMember $i++ [int]$pct = ($i / $iMemberCount) * 100 #.Net garbage collection every 100 members if (($i % 100) -eq 0) { [system.gc]::Collect() } Try { [string]$memberName = ($oMember).name } Catch { [string]$memberName = ($oMember).SamAccountname } if ($i -eq $iMemberCount) { $ProgParam = @{ Activity = "Getting requested properties for members ..." Completed = $true } } Else { $ProgParam = @{ Activity = "Getting members of $domainGroupname" CurrentOperation = "$i of $iMemberCount" Status = 'Working ...' PercentComplete = $pct } } Write-Progress @progParam # Check if this member is a group. If ($objMember.Class -eq "group") { # retrieve group object, either, from cache (if is already cached) or from Active Directory if ($ADGroupCache.values -contains $memberName) { Write-Warning "Group $memberName seen before, skipping" $ADGroupList.Remove($memberName) } Else { $ADGroupCache.Add($oMember.DistinguishedName, $memberName) if ($GetNested) { $ADGroupList.Add($oMember.DistinguishedName, $objMember) } } } } } Catch { write-warning $_.Exception.Message } $ADGroupList.Remove([string]$gDn) $script:bFirstRun = $false } 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 #all groups hash $script:ADGroupCache = [ordered]@{ } #to do list hash $script:ADGroupList = [ordered]@{} $Groupname = ($GroupDN -Split (',|='))[1] $desktop = [environment]::GetFolderPath('Desktop') if ($GetNested) { $a = '_All' } $ReportFile = "$groupname Members$a`.csv" #Splatting Get-SaveFile parameters $params = @{ Prompt = "Select name and path of CSV user export file" Filter = "CSV Files (*.csv)|*.csv" InitialDirectory = $desktop DefaultFile = $ReportFile } $ReportFile = Get-SaveFile @params if ($reportfile.Length -le 3) { Exit } #message for instructions menu $msg = @" This script exports the members of a group with user 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. 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 = "Script Welcome and 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 member properties in order desired in output." if ($RequestedPropList.Count -ge 200) { 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 -Match 'domain|canonicalName') { if ($RequestedPropList -notcontains 'distinguishedName') { [void]$PropList.Add('distinguishedName') } [regex]::Matches( $RequestedPropList, 'domain|canonicalName') | ForEach-Object { [void]$PropList.Remove($_.value) } } $GroupDN = ($groupDN).replace('/', '\/') $Group = [ADSI]"LDAP://$GroupDN" Write-Host "Getting the membership of $GroupName" -ForegroundColor Green $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("if (`$_.Type -match 'user|computer'){![bool]((`$_.userAccountControl -band 2) -eq 2)}" ) }) | out-null break } 'smartCardLogonRequired' { $selectProps.add(@{ Name = 'smartCardLogonRequired' Expression = [Scriptblock]::Create("if(`$_.Type -eq 'user'){[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 } 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 } } } } if ($GetNested) { $selectProps.add('Type') | Out-Null $selectProps.Add('ParentGroup') | Out-Null } <# Use array to keep all in the pipeline. Extremely large groups created problems when this was saved a variable. The do loop works as main function manages ADGroupList #> #Top group $data = Get-DomainGroupMember $Group if ($GetNested -and $adgrouplist.values.count -gt 0) { #Then any groups found as members do { $data += Get-DomainGroupMember $($ADGroupList.values)[0] } while ($ADGroupList.count -gt 0) } $data | Select-Object $SelectProps | Sort-Object -Property $SortBy | export-csv $ReportFile -NoTypeInformation Write-Progress -Completed "Done" #If more than 1500 members, show elapsed time if ($ScriptStart) { $elapsedTimeSpan = $ScriptStart - $(Get-Date) $elapsedTime = 'Elapsed Time: ' + $([string]::Format("{0:D2} hrs, {1:D2} mins, {2:D2} secs", $elapsedTimeSpan.Hours, $elapsedTimeSpan.Minutes, $elapsedTimeSpan.Seconds)) + ".`n`n" } $msg = "Done. $elapsedTime`Do you want to open $ReportFile now?" $retval = [Microsoft.VisualBasic.Interaction]::MsgBox($msg, 'YesNo,defaultbutton1,Question', "Open Report") If ($retval -eq 'Yes') { Invoke-Item $ReportFile }