PSWorkbench.psm1
|
#Region '.\Public\Get-ADBulkUserHashtable.ps1' -1 function Get-ADBulkUserHashtable { <# .SYNOPSIS Retrieves Active Directory user information for multiple users from primary and backup domains .DESCRIPTION Searches for multiple AD users simultaneously using an optimized LDAP filter with flexible search criteria. If users are not found in the default domain, performs a secondary search against a specified global catalog server. Returns user details in a hashtable for efficient lookups. Uses verbose output to report users not found in either domain. .PARAMETER UserList Array of user identifiers to search for in Active Directory. The identifier type is determined by the SearchBy parameter. .PARAMETER SearchBy Specifies which AD attribute to use for user lookups. Use 'Auto' (default) to detect per-value based on format: values containing '@' resolve to UserPrincipalName, 'CN=' prefix to DistinguishedName, all-digits to EmployeeID, whitespace to DisplayName, and everything else to SamAccountName. Mixed input lists are fully supported in Auto mode. Note: EmployeeID and Mail are not in the default Get-ADUser property set - include them via -Properties if needed. .PARAMETER Server Active Directory server to target for the primary search. When omitted, the default domain controller is used. .PARAMETER ADGlobalCatalog Global catalog server and port for backup domain searches when users are not found in the default domain. .PARAMETER Properties Optional array of additional AD properties to retrieve for each user .EXAMPLE Get-ADBulkUserHashtable -UserList 'user1', 'user2', 'user3' Searches for three users using Auto detection (all resolve to SamAccountName) .EXAMPLE Get-ADBulkUserHashtable -UserList 'user1@company.com', 'jdoe', 'CN=Jane Smith,OU=Users,DC=corp,DC=com' Mixed input list: UPN, SamAccountName, and DistinguishedName resolved automatically via Auto detection .EXAMPLE Get-ADBulkUserHashtable -UserList 'user1@company.com', 'user2@company.com' -SearchBy 'UserPrincipalName' -Verbose Explicit SearchBy - all values searched by UPN with verbose output for tracking search results .EXAMPLE Get-ADBulkUserHashtable -UserList 'John Doe', 'Jane Smith' -SearchBy 'DisplayName' -Properties 'Department', 'Title' Retrieves users by display name with additional properties .NOTES Author: https://github.com/dan-metzler PowerShellVersion: PowerShell 5.1 or Later Recommended. Features: Bulk user lookup, flexible search criteria, automatic failover to global catalog, hashtable return for efficient lookups, verbose logging #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$UserList, [Parameter()] [ValidateSet("Auto", "SamAccountName", "UserPrincipalName", "Mail", "DisplayName", "EmployeeID", "DistinguishedName")] [string]$SearchBy = "Auto", # Parameter help description [Parameter()] [string]$Server, # this parameter is when users are not found in the default domain, we check a backup domain, this is the server for the backup domain. [Parameter()] [string]$ADGlobalCatalog, # Optional properties parameter [Parameter()] [string[]]$Properties ) # Map parameter values to actual LDAP attribute names $ldapAttributeMap = @{ "SamAccountName" = "sAMAccountName" "UserPrincipalName" = "userPrincipalName" "Mail" = "mail" "DisplayName" = "displayName" "EmployeeID" = "employeeID" "DistinguishedName" = "distinguishedName" } # When SearchBy is Auto, detect each input value's attribute type by its distinct form: # ^\s*(?i:(CN|OU|DC|...)) -> DistinguishedName | ^[^@\s]+@[^\s]+$ -> UserPrincipalName # ^\d+$ -> EmployeeID | whitespace -> DisplayName | default -> SamAccountName if ($SearchBy -eq 'Auto') { $inputAttributeMap = @{} foreach ($inputValue in $UserList) { $inputAttributeMap[$inputValue] = switch -Regex ($inputValue) { '^\s*(?i:(CN|OU|DC|O|L|ST|C)=)' { 'DistinguishedName'; break } '^[^@\s]+@[^\s]+$' { 'UserPrincipalName'; break } '^\d+$' { 'EmployeeID'; break } '\s' { 'DisplayName'; break } default { 'SamAccountName' } } } } else { $ldapAttribute = $ldapAttributeMap[$SearchBy] } # DisplayName, EmployeeID, and Mail are not in the default Get-ADUser property set. # In Auto mode, auto-inject whichever of those were detected so the post-search matching # can read them back from the returned user objects. $effectiveProperties = $Properties if ($SearchBy -eq 'Auto') { $autoProps = ($inputAttributeMap.Values | Select-Object -Unique | Where-Object { $_ -in 'DisplayName', 'EmployeeID', 'Mail' }) if ($autoProps) { $effectiveProperties = @($autoProps) + @($Properties | Where-Object { $_ }) | Select-Object -Unique } Write-Verbose "Auto-detected attribute types: $(($inputAttributeMap.GetEnumerator() | ForEach-Object { "'$($_.Key)' -> '$($_.Value)'" }) -join ' | ')" } # Construct LDAP filter - in Auto mode each value uses its detected attribute; otherwise all share the same attribute $hashtable = @{} if ($SearchBy -eq 'Auto') { $ldapFilter = "(|" + ($UserList | ForEach-Object { "($($ldapAttributeMap[$inputAttributeMap[$_]])=$_)" }) + ")" } else { $ldapFilter = "(|" + ($UserList | ForEach-Object { "($ldapAttribute=$_)" }) + ")" } # Use splatting to conditionally include Properties parameter $getUserParams = @{ LDAPFilter = $ldapFilter } if ($effectiveProperties) { $getUserParams['Properties'] = $effectiveProperties } if ($Server) { $getUserParams['Server'] = $Server } $userDetailsList = @(Get-ADUser @getUserParams) # Create lookup mapping from search input to found users $searchInputToUser = @{} for ($i = 0; $i -lt $userDetailsList.Count; $i++) { $currentUser = $userDetailsList[$i] # Find which input value(s) match this user - in Auto mode each input value checks its own detected attribute $matchingInputs = $UserList | Where-Object { $attr = if ($SearchBy -eq 'Auto') { $inputAttributeMap[$_] } else { $SearchBy } $currentUser.$attr -eq $_ } foreach ($input in $matchingInputs) { $searchInputToUser[$input] = $currentUser } } # add the results to the hashtable keyed by SamAccountName for consistent lookups regardless of input form foreach ($inputValue in $UserList) { if ($searchInputToUser.ContainsKey($inputValue)) { $hashtable[$searchInputToUser[$inputValue].SamAccountName] = $searchInputToUser[$inputValue] } } if ($hashtable.Count -ne $UserList.Count) { # Use the input-tracking map (not the hashtable) to find which inputs didn't resolve to a user $notFoundUsers = $UserList | Where-Object { -not $searchInputToUser.ContainsKey($_) } Write-Verbose "$($notFoundUsers.Count) users were not found in Default Active Directory, searching global catalog..." # for users not found in the original search we need a sub search against the global catalog, we can reuse the same parameters but need to change the server and ldap filter if ($SearchBy -eq 'Auto') { $notFoundUsersLdapFilter = "(|" + ($notFoundUsers | ForEach-Object { "($($ldapAttributeMap[$inputAttributeMap[$_]])=$_)" }) + ")" } else { $notFoundUsersLdapFilter = "(|" + ($notFoundUsers | ForEach-Object { "($ldapAttribute=$_)" }) + ")" } $getUserParams['Server'] = $ADGlobalCatalog $getUserParams['LDAPFilter'] = $notFoundUsersLdapFilter $userBackupDetailsList = @(Get-ADUser @getUserParams) # add the backup results to the hashtable, nothing added if the userBackupDetailsList is empty $backupSearchInputToUser = @{} for ($i = 0; $i -lt $userBackupDetailsList.Count; $i++) { $currentUser = $userBackupDetailsList[$i] # Find which input value(s) match this user from backup search - same Auto-mode attribute detection $matchingInputs = $notFoundUsers | Where-Object { $attr = if ($SearchBy -eq 'Auto') { $inputAttributeMap[$_] } else { $SearchBy } $currentUser.$attr -eq $_ } foreach ($input in $matchingInputs) { $backupSearchInputToUser[$input] = $currentUser } } # add the backup results keyed by SamAccountName foreach ($inputValue in $notFoundUsers) { if ($backupSearchInputToUser.ContainsKey($inputValue)) { $hashtable[$backupSearchInputToUser[$inputValue].SamAccountName] = $backupSearchInputToUser[$inputValue] } } # Check if there are still users not found after backup search $stillNotFoundUsers = $notFoundUsers | Where-Object { -not $backupSearchInputToUser.ContainsKey($_) } if ($stillNotFoundUsers.Count -gt 0) { foreach ($user in $stillNotFoundUsers) { Write-Warning "User not found in ($env:USERDNSDOMAIN) or ($ADGlobalCatalog): $user" } } } if ($hashtable.count -gt 0) { $hashtable } else { $null } } #EndRegion '.\Public\Get-ADBulkUserHashtable.ps1' 215 #Region '.\Public\Resolve-ADGroupMember.ps1' -1 function Resolve-ADGroupMember { <# .SYNOPSIS Retrieves Active Directory group members with cross-domain resolution capabilities to handle complex multi-domain environments. .DESCRIPTION This function addresses common challenges in multi-domain Active Directory environments by retrieving group members and automatically handling cross-domain member resolution. It queries the specified Active Directory groups for their membership details, attempts to resolve each member object in the local domain first, and falls back to global catalog queries when members exist in different domains. The function provides robust error handling for orphaned or inaccessible member references and returns detailed member object information for analysis and reporting. .PARAMETER Identity Array of Active Directory group identities (names, distinguished names, or SIDs) for which to retrieve membership information. Supports pipeline input and processes multiple groups efficiently with cross-domain member resolution. .PARAMETER ADGlobalCatalog Global catalog server for cross-domain member resolution. Used as a fallback when a member object cannot be found in the local domain. .EXAMPLE Resolve-ADGroupMember -Identity "Domain-SecurityGroup-Name" Retrieves all members of the specified security group, resolving members across domains as needed. .EXAMPLE "Group1", "Group2", "Group3" | Resolve-ADGroupMember Uses pipeline input to process multiple groups and return comprehensive member information with cross-domain resolution. .NOTES Author: https://github.com/dan-metzler PowerShellVersion: PowerShell 5.1 or Later Recommended. Features: Cross-domain resolution, Global catalog queries, Pipeline support, Error handling, Multi-group processing #> [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0 )] [string[]]$Identity, # Active Directory global catalog server for cross-domain member resolution when local queries fail [Parameter(Mandatory)] [string]$ADGlobalCatalog ) foreach ($GroupIdentity in $Identity) { $Group = $null $Group = Get-ADGroup -Identity $GroupIdentity -Properties Member if (-not $Group) { continue } Foreach ($Member in $Group.Member) { try { Get-ADObject $Member } catch { if ($_.Exception.GetType().FullName -eq 'Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException') { Get-ADObject $Member -Server $ADGlobalCatalog } else { Write-Error "Error finding $Member in Root Global Domains" } } } } } #EndRegion '.\Public\Resolve-ADGroupMember.ps1' 73 #Region '.\Public\Update-GenericList.ps1' -1 function Update-GenericList { <# .SYNOPSIS Sanitizes and normalizes user input arrays by removing whitespaces, converting case, and filtering null/duplicate values .DESCRIPTION The **Update-GenericList** function processes user input arrays to standardize and clean the data. It provides flexible options to remove whitespaces, convert case (uppercase or lowercase), remove null/empty items, and eliminate duplicates. .PARAMETER UserInput Array of strings that require sanitization and normalization. This parameter is mandatory and accepts empty collections. .PARAMETER RemoveWhitespaces Switch parameter to remove all whitespace characters from each input item using regex replacement. .PARAMETER Trim Switch parameter to remove leading and trailing whitespace from each input item while preserving internal spaces. .PARAMETER ConvertToLowercase Switch parameter to convert all input items to lowercase. Cannot be used simultaneously with ConvertToUppercase. .PARAMETER ConvertToUppercase Switch parameter to convert all input items to uppercase. Cannot be used simultaneously with ConvertToLowercase. .PARAMETER RemoveNullOrEmptyItems Switch parameter to filter out null, empty, or whitespace-only items from the input array. .PARAMETER RemoveDuplicates Switch parameter to remove duplicate values from the processed array using Select-Object -Unique. .EXAMPLE Update-GenericList -UserInput @(" User1 ", "USER2", "user1") -RemoveWhitespaces -ConvertToLowercase -RemoveDuplicates Returns: @("user1", "user2") - removes spaces, converts to lowercase, and eliminates duplicates .EXAMPLE Update-GenericList -UserInput @("account1", "", "ACCOUNT2", $null) -ConvertToUppercase -RemoveNullOrEmptyItems Returns: @("ACCOUNT1", "ACCOUNT2") - converts to uppercase and removes null/empty items .EXAMPLE Update-GenericList -UserInput @(" John Doe ", " Jane Smith ", "Bob Jones") -Trim -RemoveDuplicates Returns: @("John Doe", "Jane Smith", "Bob Jones") - removes leading/trailing spaces while preserving internal spaces .NOTES Author: https://github.com/dan-metzler PowerShellVersion: PowerShell 5.1 or Later Recommended Features: - Flexible input sanitization with multiple processing options - Parameter validation prevents conflicting case conversion options - Efficient processing using .NET collections and regex - Verbose logging for processing operations performed - Support for empty collections and null handling #> [CmdletBinding()] param ( [Parameter( Mandatory = $true, HelpMessage = "Array of strings that require sanitization and normalization." )] [AllowEmptyCollection()] [AllowEmptyString()] [string[]]$UserInput, [Parameter( )] [switch]$RemoveWhitespaces, [Parameter( )] [switch]$Trim, [Parameter()] [switch]$ConvertToLowercase, [Parameter()] [switch]$ConvertToUppercase, [Parameter()] [switch]$RemoveNullOrEmptyItems, [Parameter()] [switch]$RemoveDuplicates ) # PARAMETER VALIDATION, ONE MUST BE SET TO TRUE if (-Not($RemoveWhitespaces -or $Trim -or $ConvertToLowercase -or $ConvertToUppercase)) { throw "At least one of the boolean parameters [RemoveWhitespaces, Trim, ConvertToLowercase, ConvertToUppercase] must be set to `$true" } if ($ConvertToLowercase -and $ConvertToUppercase) { throw "[ConvertToLowercase] and [ConvertToUppercase] CANNOT both be set at the same time, only one can be chosen" } # IF WHITE SPACE SWITCH IS SELECTED, CLEAN THE INPUT DATA AND GET RID OF ANY SPACES $LIST_CollectUpdatedItems = [System.Collections.Generic.List[string]]::New() if ($RemoveWhitespaces -and $ConvertToLowercase) { for ($i = 0; $i -lt $UserInput.Count; $i++) { $LIST_CollectUpdatedItems.Add($UserInput[$i].Replace(' ', '').ToLowerInvariant()) } Write-Verbose "Cleaned User Input :: [Removed Whitespaces & Converted To Lowercase]" } elseif ($RemoveWhitespaces -and $ConvertToUppercase) { for ($i = 0; $i -lt $UserInput.Count; $i++) { $LIST_CollectUpdatedItems.Add($UserInput[$i].Replace(' ', '').ToUpperInvariant()) } Write-Verbose "Cleaned User Input :: [Removed Whitespaces & Converted To Uppercase]" } elseif ($RemoveWhitespaces) { for ($i = 0; $i -lt $UserInput.Count; $i++) { $LIST_CollectUpdatedItems.Add($UserInput[$i].Replace(' ', '')) } Write-Verbose "Cleaned User Input :: [Removed Whitespaces Only]" } elseif ($ConvertToLowercase -and (-Not($RemoveWhitespaces))) { for ($i = 0; $i -lt $UserInput.Count; $i++) { $LIST_CollectUpdatedItems.Add($UserInput[$i].ToLowerInvariant()) } Write-Verbose "Cleaned User Input :: [Converted To Lowercase]" } elseif ($ConvertToUppercase -and (-Not($RemoveWhitespaces))) { for ($i = 0; $i -lt $UserInput.Count; $i++) { $LIST_CollectUpdatedItems.Add($UserInput[$i].ToUpperInvariant()) } Write-Verbose "Cleaned User Input :: [Converted To Uppercase]" } elseif ($Trim -and (-Not($RemoveWhitespaces -or $ConvertToLowercase -or $ConvertToUppercase))) { for ($i = 0; $i -lt $UserInput.Count; $i++) { $LIST_CollectUpdatedItems.Add($UserInput[$i].Trim()) } Write-Verbose "Cleaned User Input :: [Trimmed Leading/Trailing Whitespace]" } # Apply Trim processing if specified along with other operations if ($Trim -and ($ConvertToLowercase -or $ConvertToUppercase -or $RemoveWhitespaces)) { for ($i = 0; $i -lt $LIST_CollectUpdatedItems.Count; $i++) { $LIST_CollectUpdatedItems[$i] = $LIST_CollectUpdatedItems[$i].Trim() } Write-Verbose "Cleaned User Input :: [Trimmed Leading/Trailing Whitespace]" } if ($RemoveNullOrEmptyItems) { $revised_list = [System.Collections.Generic.List[string]]::New() $removed_null_empty_counter = 0 for ($i = 0; $i -lt $LIST_CollectUpdatedItems.Count; $i++) { if ([string]::IsNullOrEmpty($LIST_CollectUpdatedItems[$i].Trim())) { #Write-Verbose "String is Null or Empty, Removed From List :: [$i]" $removed_null_empty_counter++ } else { $revised_list.Add($LIST_CollectUpdatedItems[$i]) } } if ($removed_null_empty_counter -ne 0) { Write-Verbose "Number of null or empty items removed from list :: [$($removed_null_empty_counter)]" } if ($RemoveDuplicates) { $deduped = [System.Collections.Generic.List[string]]::new() foreach ($item in ($revised_list | Select-Object -Unique)) { $deduped.Add($item) } Write-Output -NoEnumerate $deduped } else { Write-Output -NoEnumerate $revised_list } } else { if ($RemoveDuplicates) { $deduped = [System.Collections.Generic.List[string]]::new() foreach ($item in ($LIST_CollectUpdatedItems | Select-Object -Unique)) { $deduped.Add($item) } Write-Output -NoEnumerate $deduped } else { Write-Output -NoEnumerate $LIST_CollectUpdatedItems } } } #EndRegion '.\Public\Update-GenericList.ps1' 188 |