Private/Convert/Convert-IdentityReferenceToNTAccount.ps1
|
function Convert-IdentityReferenceToNTAccount { <# .SYNOPSIS Converts a SecurityIdentifier (SID) to an IdentityReference (NTAccount). .DESCRIPTION Takes a System.Security.Principal.SecurityIdentifier object and converts it to an NTAccount object. If the input is already an NTAccount, it is returned unchanged. On domain-joined computers, uses the built-in Translate() method. On non-domain joined computers, performs an LDAP query using provided credentials to resolve the SID to an NTAccount. Supports forest-wide searches using Global Catalog when RootDSE is provided, enabling resolution of principals from child domains and trusted domains within the forest. .PARAMETER SecurityIdentifier The SecurityIdentifier object to convert. Typically from SID strings or ACL entries. .PARAMETER Credential PSCredential for authenticating to Active Directory. Required when running from non-domain joined computers. .PARAMETER RootDSE A DirectoryEntry object for the RootDSE. Used to determine the domain context for LDAP queries. If not specified, attempts to query without specific domain context. .INPUTS System.Security.Principal.SecurityIdentifier Accepts SecurityIdentifier objects via the pipeline. .OUTPUTS System.Security.Principal.NTAccount Returns the NTAccount representation of the SID, or the original object if already an NTAccount. .EXAMPLE $sid = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-21-...') $sid | Convert-IdentityReferenceToNTAccount Converts a SID to NTAccount (domain-joined computer). .EXAMPLE $sid | Convert-IdentityReferenceToNTAccount -Credential $cred -RootDSE $rootDSE Converts a SID to NTAccount using credentials and RootDSE (non-domain joined computer). .EXAMPLE $ace.IdentityReference | Convert-IdentityReferenceToNTAccount -Credential $cred -RootDSE $rootDSE Converts SID IdentityReferences from an ACL to NTAccount objects. .NOTES Automatically detects domain membership and uses appropriate method. For non-domain joined scenarios, Credential and RootDSE parameters are recommended. Uses Global Catalog for forest-wide searches to support child domain resolution. #> [CmdletBinding()] [OutputType([System.Security.Principal.NTAccount])] param( [Parameter(Mandatory, ValueFromPipeline)] [System.Security.Principal.IdentityReference] $SecurityIdentifier, [Parameter()] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.DirectoryServices.DirectoryEntry] $RootDSE ) process { # If already an NTAccount, return it if ($SecurityIdentifier -is [System.Security.Principal.NTAccount]) { return $SecurityIdentifier } # Check PrincipalStore first (if it exists) - it has the NTAccount name already $sidString = $SecurityIdentifier.Value if ($script:PrincipalStore -and $script:PrincipalStore.ContainsKey($sidString)) { $storedPrincipal = $script:PrincipalStore[$sidString] if ($storedPrincipal.ntAccountName) { Write-Verbose "PrincipalStore HIT for SID '$sidString' → NTAccount: $($storedPrincipal.ntAccountName)" return [System.Security.Principal.NTAccount]::new($storedPrincipal.ntAccountName) } } # Try the built-in Translate method first (works on domain-joined computers) try { $ntAccount = $SecurityIdentifier.Translate([System.Security.Principal.NTAccount]) return $ntAccount } catch { Write-Verbose "Translate() failed, attempting LDAP lookup: $_" } # Fallback to LDAP query for non-domain joined scenarios if (-not $Credential) { Write-Warning "Could not translate SID '$SecurityIdentifier' to NTAccount. Not domain-joined and no credential provided." return $SecurityIdentifier } try { # Get the SID string $sidString = $SecurityIdentifier.Value # First try Global Catalog search for forest-wide lookup Write-Verbose "Attempting Global Catalog search for SID '$sidString'" $gcSearcher = New-GCSearcher -Filter "(objectSid=$sidString)" -PropertiesToLoad @('distinguishedName', 'sAMAccountName') if ($gcSearcher) { try { $gcResult = $gcSearcher.FindOne() if ($gcResult -and $gcResult.Properties['sAMAccountName'].Count -gt 0) { $distinguishedName = $gcResult.Properties['distinguishedName'][0] $samAccountName = $gcResult.Properties['sAMAccountName'][0] Write-Verbose "Found SID in GC at: $distinguishedName" # Get NetBIOS domain name from DomainStore $domainDN = $distinguishedName -replace '^.*?,(?=DC=)', '' if ($script:DomainStore -and $script:DomainStore.ContainsKey($domainDN)) { $domainNetBiosName = $script:DomainStore[$domainDN].nETBIOSName.ToUpper() } else { # Fallback: extract first DC component from DN if ($domainDN -match 'DC=([^,]+)') { $domainNetBiosName = $Matches[1].ToUpper() Write-Verbose "Using fallback NetBIOS name from DN: $domainNetBiosName" } else { $domainNetBiosName = 'UNKNOWN' } } $ntAccountString = "$($domainNetBiosName.ToUpper())\$samAccountName" $ntAccount = New-Object System.Security.Principal.NTAccount($ntAccountString) Write-Verbose "Resolved SID '$sidString' to '$ntAccountString' via Global Catalog" return $ntAccount } } catch { Write-Verbose "Global Catalog search failed, falling back to domain search: $_" } finally { if ($gcSearcher) { $gcSearcher.Dispose() } } } # Fallback to direct LDAP search in default domain Write-Verbose "Attempting direct LDAP search for SID '$sidString'" $domainDN = if ($RootDSE) { $RootDSE.defaultNamingContext.Value } else { $null } if (-not $domainDN) { Write-Warning "Could not determine domain DN for SID resolution." return $SecurityIdentifier } # Create LDAP searcher with credentials $searcher = New-LDAPSearcher -DomainDN $domainDN -Filter "(objectSid=$sidString)" -PropertiesToLoad @('sAMAccountName', 'distinguishedName') $result = $searcher.FindOne() if ($result -and $result.Properties['sAMAccountName'].Count -gt 0) { $samAccountName = $result.Properties['sAMAccountName'][0] $distinguishedName = $result.Properties['distinguishedName'][0] # Get NetBIOS domain name from DomainStore $domainDN = $distinguishedName -replace '^.*?,(?=DC=)', '' if ($script:DomainStore -and $script:DomainStore.ContainsKey($domainDN)) { $domainNetBiosName = $script:DomainStore[$domainDN].nETBIOSName.ToUpper() } else { # Fallback: extract first DC component from DN if ($domainDN -match 'DC=([^,]+)') { $domainNetBiosName = $Matches[1].ToUpper() Write-Verbose "Using fallback NetBIOS name from DN: $domainNetBiosName" } else { $domainNetBiosName = 'UNKNOWN' } } $ntAccountString = "$($domainNetBiosName.ToUpper())\$samAccountName" $ntAccount = New-Object System.Security.Principal.NTAccount($ntAccountString) Write-Verbose "Resolved SID '$sidString' to '$ntAccountString' via LDAP" return $ntAccount } else { Write-Warning "Could not find SID '$sidString' in Active Directory via LDAP query." return $SecurityIdentifier } } catch { Write-Warning "LDAP query failed for SID '$sidString': $_" return $SecurityIdentifier } finally { if ($searcher) { $searcher.Dispose() } } } } |