Private/Set/Set-Owner.ps1

function Set-Owner {
    <#
        .SYNOPSIS
        Normalizes and resolves owner identity references for AD CS objects.

        .DESCRIPTION
        Examines the Owner property of AD CS objects and ensures consistent SID resolution
        by calling Resolve-Principal to populate the PrincipalStore cache. This enables
        downstream vulnerability detection functions (ESC4o, ESC5o) to consistently resolve
        owner identities to human-readable names.
        
        The Owner property from ObjectSecurity.Owner can return inconsistent formats:
        - DOMAIN\User (resolved NTAccount)
        - S-1-5-21-... (raw SID string)
        - O:S-1-5-21-... (SDDL format)
        
        This function extracts the SID, calls Resolve-Principal to cache the principal,
        and normalizes the Owner property to a consistent SID string format that can be
        reliably resolved later via Convert-IdentityReferenceToNTAccount.
        
        This is a critical preprocessing step for ESC4o and ESC5o vulnerability detection,
        ensuring that owner identities can be consistently resolved regardless of the
        format returned by the .NET ObjectSecurity.Owner property.

        .PARAMETER AdcsObject
        One or more LS2AdcsObject instances representing AD CS objects.

        .INPUTS
        LS2AdcsObject[]
        You can pipe LS2AdcsObject instances to this function.

        .OUTPUTS
        LS2AdcsObject[]
        Returns the input objects with normalized Owner properties.

        .EXAMPLE
        $templates = $script:AdcsObjectStore.Values |
            Where-Object { $_.objectClass -contains 'pKICertificateTemplate' }
        $templates | Set-Owner
        Normalizes owner identities for all certificate templates.

        .EXAMPLE
        $script:AdcsObjectStore.Values | Set-Owner
        Normalizes owner identities for all AD CS objects in the store.

        .NOTES
        This function mirrors the ACE resolution pattern used in Set-DangerousEditor,
        Set-LowPrivilegeEnrollee, etc., by proactively calling Resolve-Principal to
        populate PrincipalStore before owner names are needed for display.
        
        Run this function in the Initialize-AdcsObjectStore pipeline after
        Initialize-PrincipalDefinitions has completed and PrincipalStore is ready.
    #>

    [CmdletBinding()]
    [OutputType([LS2AdcsObject[]])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [LS2AdcsObject[]]$AdcsObject
    )

    #requires -Version 5.1

    begin {
        Write-Verbose "Normalizing owner identities for AD CS objects..."
    }

    process {
        $AdcsObject | ForEach-Object {
            try {
                $objectName = if ($_.displayName) {
                    $_.displayName
                } elseif ($_.Name) {
                    $_.Name
                } else {
                    $_.DistinguishedName
                }
                Write-Verbose "Processing owner for: $objectName"
                
                $owner = $_.Owner
                
                if (-not $owner) {
                    Write-Verbose "No owner found for object: $objectName"
                    return $_
                }
                
                # Extract SID from owner string (handles raw SID, SDDL format, or NTAccount)
                $ownerSid = $null
                
                if ($owner -match '^(?:O:)?(S-1-[\d-]+)') {
                    # Owner is already a SID (with or without SDDL prefix)
                    try {
                        $ownerSid = [System.Security.Principal.SecurityIdentifier]::new($Matches[1])
                        Write-Verbose "Owner is SID format: $($ownerSid.Value)"
                    } catch {
                        Write-Warning "Invalid SID format for owner '$owner' on object '$objectName': $_"
                    }
                } else {
                    # Owner is in NTAccount format (DOMAIN\User) - convert to SID
                    try {
                        $ntAccount = [System.Security.Principal.NTAccount]::new($owner)
                        $ownerSid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
                        Write-Verbose "Converted NTAccount '$owner' to SID: $($ownerSid.Value)"
                    } catch {
                        Write-Warning "Could not translate NTAccount '$owner' to SID for object '$objectName': $_"
                    }
                }
                
                # If we successfully extracted a SID, resolve it to populate PrincipalStore
                if ($ownerSid) {
                    Write-Verbose "Resolving owner SID to populate PrincipalStore: $($ownerSid.Value)"
                    $null = $ownerSid | Resolve-Principal
                    
                    # Normalize the Owner property to the SID string
                    # This ensures downstream code can reliably convert it via PrincipalStore
                    $_.Owner = $ownerSid.Value
                    
                    Write-Verbose "Normalized owner to SID: $($ownerSid.Value)"
                }
                
                # Return the modified object
                $_
                
            } catch {
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    $_.Exception,
                    'OwnerResolutionFailed',
                    [System.Management.Automation.ErrorCategory]::InvalidOperation,
                    $_
                )
                $PSCmdlet.WriteError($errorRecord)
                
                # Still return the object even if processing failed
                $_
            }
        }
    }

    end {
        Write-Verbose "Owner normalization complete."
    }
}