#
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 }