Private/IdentityHelpers.ps1
|
<#
.SYNOPSIS Locale-invariant identity helpers (private). .DESCRIPTION SID-based lookups that survive non-English Windows, 32-bit PowerShell on 64-bit Windows (LocalAccounts module missing), domain controllers (no local SAM), and unresolvable SIDs (AzureAD-joined machines, PowerShell#2996). All functions degrade to empty/$null/$false instead of throwing. #> # False = last admin-group lookup failed (unknown), NOT 'zero admins' $script:AdminGroupLookupSucceeded = $false function Get-WellKnownSid { [CmdletBinding()] [OutputType([hashtable])] param() return @{ Administrators = 'S-1-5-32-544' Users = 'S-1-5-32-545' Guests = 'S-1-5-32-546' BackupOperators = 'S-1-5-32-551' RemoteDesktopUsers = 'S-1-5-32-555' System = 'S-1-5-18' LocalService = 'S-1-5-19' NetworkService = 'S-1-5-20' Everyone = 'S-1-1-0' AuthenticatedUsers = 'S-1-5-11' } } function Get-AdminGroupMembers { [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter()] [string]$Sid = 'S-1-5-32-544' ) $script:AdminGroupLookupSucceeded = $false $members = @() # Preferred: LocalAccounts module, addressed by SID (locale-invariant). # NOTE: module is missing in 32-bit PowerShell on 64-bit Windows. if (Get-Command 'Get-LocalGroupMember' -ErrorAction SilentlyContinue) { try { $sidObject = [Security.Principal.SecurityIdentifier]$Sid $raw = @(Get-LocalGroupMember -SID $sidObject -ErrorAction Stop) foreach ($member in $raw) { $memberName = $null $memberSid = $null $memberClass = $null if ($null -ne $member) { if ($member.PSObject.Properties['Name']) { $memberName = $member.Name } if ($member.PSObject.Properties['SID'] -and $null -ne $member.SID) { $memberSid = $member.SID.Value } if ($member.PSObject.Properties['ObjectClass']) { $memberClass = $member.ObjectClass } } $members += [PSCustomObject]@{ Name = $memberName SID = $memberSid ObjectClass = $memberClass } } $script:AdminGroupLookupSucceeded = $true return $members } catch { # Includes the known 'Failed to compare two elements' bug on # unresolvable SIDs (AzureAD-joined machines, PowerShell#2996) Write-Verbose "Get-AdminGroupMembers: Get-LocalGroupMember failed, trying ADSI: $($_.Exception.Message)" } } # Fallback: ADSI WinNT provider. SID -> localized group name, then enumerate. try { $groupName = ([Security.Principal.SecurityIdentifier]$Sid).Translate( [Security.Principal.NTAccount]).Value.Split('\')[-1] } catch { Write-Verbose "Get-AdminGroupMembers: cannot translate SID $Sid : $($_.Exception.Message)" return $members } try { $group = ([ADSI]"WinNT://./$groupName,group") $rawMembers = @($group.psbase.Invoke('Members')) foreach ($member in $rawMembers) { $memberName = $null $memberSid = $null $memberClass = $null try { $memberName = $member.GetType().InvokeMember('Name', 'GetProperty', $null, $member, $null) } catch { Write-Verbose "Get-AdminGroupMembers: member Name unreadable: $($_.Exception.Message)" } try { $memberClass = $member.GetType().InvokeMember('Class', 'GetProperty', $null, $member, $null) } catch { Write-Verbose "Get-AdminGroupMembers: member Class unreadable: $($_.Exception.Message)" } try { $memberSidBytes = [byte[]]($member.GetType().InvokeMember('objectSID', 'GetProperty', $null, $member, $null)) $memberSid = (New-Object Security.Principal.SecurityIdentifier($memberSidBytes, 0)).Value } catch { Write-Verbose "Get-AdminGroupMembers: member objectSID unreadable: $($_.Exception.Message)" } $members += [PSCustomObject]@{ Name = $memberName SID = $memberSid ObjectClass = $memberClass } } $script:AdminGroupLookupSucceeded = $true } catch { Write-Verbose "Get-AdminGroupMembers: ADSI enumeration failed: $($_.Exception.Message)" $members = @() } return $members } function Get-LocalUserByRid { [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [int]$Rid ) $suffix = "-$Rid" # Preferred: LocalAccounts module (missing in 32-bit PowerShell on 64-bit Windows) if (Get-Command 'Get-LocalUser' -ErrorAction SilentlyContinue) { try { $user = @(Get-LocalUser -ErrorAction Stop | Where-Object { $null -ne $_.SID -and $_.SID.Value.EndsWith($suffix) } | Select-Object -First 1) if ($user.Count -gt 0 -and $null -ne $user[0]) { return [PSCustomObject]@{ Name = $user[0].Name SID = $user[0].SID.Value Enabled = $user[0].Enabled Description = $user[0].Description } } } catch { Write-Verbose "Get-LocalUserByRid: Get-LocalUser failed: $($_.Exception.Message)" } } # Fallback: CIM Win32_UserAccount (SID values are locale-invariant) if (Get-Command 'Get-CimInstance' -ErrorAction SilentlyContinue) { try { $account = @(Get-CimInstance -ClassName 'Win32_UserAccount' -Filter 'LocalAccount=True' -ErrorAction Stop | Where-Object { $null -ne $_.SID -and $_.SID.EndsWith($suffix) } | Select-Object -First 1) if ($account.Count -gt 0 -and $null -ne $account[0]) { $enabled = $null if ($account[0].PSObject.Properties['Disabled'] -and $null -ne $account[0].Disabled) { $enabled = (-not $account[0].Disabled) } return [PSCustomObject]@{ Name = $account[0].Name SID = $account[0].SID Enabled = $enabled Description = $account[0].Description } } } catch { Write-Verbose "Get-LocalUserByRid: CIM query failed: $($_.Exception.Message)" } } # Last resort: ADSI WinNT provider, RID extracted from objectSID bytes try { $computer = [ADSI]'WinNT://.' foreach ($child in @($computer.psbase.Children)) { $schemaClass = $null try { $schemaClass = $child.GetType().InvokeMember('SchemaClassName', 'GetProperty', $null, $child, $null) } catch { continue } if ($schemaClass -ne 'User') { continue } try { $sidBytes = [byte[]]($child.GetType().InvokeMember('objectSID', 'GetProperty', $null, $child, $null)) $userSid = (New-Object Security.Principal.SecurityIdentifier($sidBytes, 0)).Value if ($userSid.EndsWith($suffix)) { $userName = $child.GetType().InvokeMember('Name', 'GetProperty', $null, $child, $null) $userFlags = 0 $description = $null try { $userFlags = [int]($child.GetType().InvokeMember('UserFlags', 'GetProperty', $null, $child, $null)) } catch { Write-Verbose "Get-LocalUserByRid: UserFlags unreadable: $($_.Exception.Message)" } try { $description = $child.GetType().InvokeMember('Description', 'GetProperty', $null, $child, $null) } catch { Write-Verbose "Get-LocalUserByRid: Description unreadable: $($_.Exception.Message)" } # ADS_UF_ACCOUNTDISABLE = 0x2 return [PSCustomObject]@{ Name = $userName SID = $userSid Enabled = (-not [bool]($userFlags -band 0x2)) Description = $description } } } catch { Write-Verbose "Get-LocalUserByRid: ADSI user unreadable: $($_.Exception.Message)" } } } catch { Write-Verbose "Get-LocalUserByRid: ADSI enumeration failed: $($_.Exception.Message)" } return $null } function Test-IdentityReferenceIsSid { [CmdletBinding()] [OutputType([bool])] param( [Parameter()] $IdentityReference, [Parameter()] [string]$Sid ) if ($null -eq $IdentityReference -or [string]::IsNullOrEmpty($Sid)) { return $false } try { if ($IdentityReference -is [Security.Principal.SecurityIdentifier]) { return ($IdentityReference.Value -eq $Sid) } $ntAccount = $null if ($IdentityReference -is [Security.Principal.NTAccount]) { $ntAccount = $IdentityReference } elseif ($IdentityReference -is [string]) { # Some ACLs carry unresolved SIDs as plain strings if ($IdentityReference -eq $Sid) { return $true } try { $ntAccount = New-Object Security.Principal.NTAccount($IdentityReference) } catch { $ntAccount = $null } } if ($null -ne $ntAccount) { try { return ($ntAccount.Translate([Security.Principal.SecurityIdentifier]).Value -eq $Sid) } catch { # Untranslatable name; fall through to raw string equality } } return ([string]$IdentityReference -eq $Sid) } catch { return $false } } |