Collectors/Get-EscAltSecurityIdentity.ps1

function Get-EscAltSecurityIdentity {
    <#
    .SYNOPSIS
        Collects account altSecurityIdentities explicit mappings + write ACLs (read-only) for ESC14.
    .DESCRIPTION
        Enumerates user/computer accounts that have a non-empty altSecurityIdentities
        attribute, and reads each account's nTSecurityDescriptor to determine who can
        write that attribute. Test-Esc14 flags weak mapping values (X509:<I>, X509:<S>,
        X509:<RFC822>) and low-priv write access to altSecurityIdentities.

        Emits objects consumed by Test-Esc14:
            Name, DN, Mappings (string[]), WriteAces (Ace[])

        Read-only: LDAP search only. No writes.

        Offline mode: pass -InputObject with records already shaped as the output
        (Name, Mappings[], WriteAces[] of parsed Ace objects); they pass through.
    .PARAMETER Server
        Optional DC/server to bind to.
    .PARAMETER DefaultNamingContext
        Optional domain NC DN (auto-detected from RootDSE when omitted).
    .PARAMETER InputObject
        Optional array of offline records (see description).
    .PARAMETER ExtraLowPrivSid
        Optional extra low-priv SIDs forwarded to the ACL parser.
    .OUTPUTS
        [pscustomobject] AltSecurityIdentity[].
    .EXAMPLE
        Get-EscAltSecurityIdentity -Verbose
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string] $Server,

        [Parameter(Mandatory = $false)]
        [string] $DefaultNamingContext,

        [Parameter(Mandatory = $false)]
        [object[]] $InputObject,

        [Parameter(Mandatory = $false)]
        [string[]] $ExtraLowPrivSid = @()
    )

    if ($PSBoundParameters.ContainsKey('InputObject') -and $null -ne $InputObject) {
        Write-EscLog -Component 'AltSecId' -Message ("Offline mode: passing through {0} fixture record(s)." -f @($InputObject).Count)
        $out = @()
        foreach ($rec in $InputObject) {
            $out += [pscustomobject]@{
                Name      = [string]$rec.Name
                DN        = [string]$rec.DN
                Mappings  = @($rec.Mappings)
                WriteAces = @($rec.WriteAces)
            }
        }
        return @($out)
    }

    $domainNc = $DefaultNamingContext
    if ([string]::IsNullOrWhiteSpace($domainNc)) {
        $domainNc = Get-EscDefaultNamingContext -Server $Server
    }
    if ([string]::IsNullOrWhiteSpace($domainNc)) {
        Write-EscLog -Component 'AltSecId' -Level Warning -Message 'Could not resolve default naming context; returning empty set.'
        return @()
    }

    $props = @('distinguishedName', 'sAMAccountName', 'name', 'altSecurityIdentities', 'nTSecurityDescriptor')
    $results = Invoke-EscLdapSearch -SearchRoot $domainNc -Filter '(&(altSecurityIdentities=*)(|(objectClass=user)(objectClass=computer)))' -PropertiesToLoad $props -SearchScope Subtree -Server $Server

    $out = @()
    foreach ($r in @($results)) {
        $bag = ConvertTo-EscPropertyBag -SearchResult $r

        $name = ''
        if ($bag.ContainsKey('samaccountname')) { $name = [string]@($bag['samaccountname'])[0] }
        elseif ($bag.ContainsKey('name')) { $name = [string]@($bag['name'])[0] }

        $dn = ''
        if ($bag.ContainsKey('distinguishedname')) { $dn = [string]@($bag['distinguishedname'])[0] }

        $mappings = @()
        if ($bag.ContainsKey('altsecurityidentities')) { $mappings = @($bag['altsecurityidentities']) }

        $writeAces = @()
        if ($bag.ContainsKey('ntsecuritydescriptor')) {
            $sd = @($bag['ntsecuritydescriptor'])[0]
            $aces = ConvertFrom-SecurityDescriptor -SecurityDescriptor $sd -Context AdObject -ExtraLowPrivSid $ExtraLowPrivSid
            foreach ($ace in @($aces)) {
                if ($null -eq $ace) { continue }
                $canWrite = ($ace.Rights -contains 'WriteProperty') -or ($ace.Rights -contains 'GenericWrite') -or ($ace.Rights -contains 'GenericAll') -or ($ace.Rights -contains 'WriteDacl')
                if ($canWrite) { $writeAces += $ace }
            }
        }

        $out += [pscustomobject]@{
            Name      = $name
            DN        = $dn
            Mappings  = @($mappings)
            WriteAces = @($writeAces)
        }
    }

    return @($out)
}