Private/Account.ps1

# The name of an account, from its number.
#
# Windows keeps an account by a number, the SID, wherever it writes one down: in the entry
# of a User Profile, as the user of an event. A Report shows the name, and never a SID
# alone. Translating the one into the other is asked of Windows, and for an account of a
# domain Windows asks a domain controller: a question that takes seconds where that does
# not answer, and that every Kind which needs a name was asking by itself - the Change
# Kind with a table of its own for the length of one reading, the reading of the User
# Profiles with none at all, in both Parts.
#
# Here it is asked once for each account in a Run.
#
# This talks to Windows. It is Gatherer machinery and is not pure: a Judge is handed the
# name, and the state that says whether there is one, in its data, and never asks. A SID
# that does not translate is an answer too, and is remembered like a name: it is the one
# that is slow to come by.
#
# What is remembered is remembered for one Run. Each Part forgets it when it begins (see
# Clear-AccountNameCache): an account that did not translate in the last Run of this
# window - the domain was not reachable, the VPN was down - is asked for again in the
# next, and one deleted since is not shown under the name it had. The two Parts of a Run
# are two processes, and each has a table of its own.

$script:AccountNameCache = @{}

function Clear-AccountNameCache {
    <#
    .SYNOPSIS
        Forgets every account name that was resolved. Called where a Part begins.
    #>

    [CmdletBinding()]
    param()

    $script:AccountNameCache = @{}
}

function Resolve-AccountName {
    <#
    .SYNOPSIS
        The name of the account with this number (SID), as Windows resolves it, and
        whether it did. Asks Windows once for each account in a Run. Decides nothing.
    .DESCRIPTION
        What comes back:
 
            Sid the number that was asked for
            Name DOMAIN\name, or '' where Windows gave none
            State 'named', or 'unresolved' where Windows did not translate the number
 
        Windows does not translate the number of an account that was deleted, nor one of
        a domain it cannot reach at the moment. Which of the two it is, it does not say,
        and neither does this.
 
        No number is nothing to resolve: Name '' and State 'unresolved', and Windows is
        not asked. Whether an account was logged at all is the caller's to know.
 
        Not pure, and not for a Judge: see the head of this file.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()][AllowEmptyString()][string]$Sid)

    $number = "$Sid".Trim()
    if (-not $number) { return [pscustomobject]@{ Sid = ''; Name = ''; State = 'unresolved' } }

    if (-not $script:AccountNameCache.ContainsKey($number)) {
        $name = ''
        try { $name = "$((New-Object System.Security.Principal.SecurityIdentifier($number)).Translate([System.Security.Principal.NTAccount]).Value)".Trim() }
        catch { $name = '' }
        $script:AccountNameCache[$number] = $name
    }

    $name = "$($script:AccountNameCache[$number])"
    [pscustomobject]@{ Sid = $number; Name = $name; State = $(if ($name) { 'named' } else { 'unresolved' }) }
}