Private/ConvertTo-KrbClientAddress.ps1

#Requires -Version 7.4

function ConvertTo-KrbClientAddress {
    <#
    .SYNOPSIS
        Normalises the IpAddress field of a Kerberos audit event to a comparable form

    .DESCRIPTION
        The KDC writes client addresses in whichever form the socket presented, so the same
        physical client appears under several spellings across a collection:

        - '::ffff:10.20.30.40' an IPv4-mapped IPv6 address
        - '10.20.30.40' the same host over a v4 socket
        - '::1' a request originating on the domain controller itself
        - '-' no address recorded

        Counting "which clients will break" means counting distinct clients, so these have to
        collapse to one key per host before the group-by. Left unnormalised, a single legacy
        application server reports as two or three affected clients and every blast-radius
        number in the report is inflated.

        IPv6 addresses that are not v4-mapped are returned as-is; there is nothing to
        normalise and lowercasing is left to the caller's grouping, which is
        case-insensitive.

    .PARAMETER Value
        [System.String] (Mandatory, Pipeline: ByValue)

        The raw IpAddress field text.

    .EXAMPLE
        PS> ConvertTo-KrbClientAddress -Value '::ffff:10.20.30.40'

        DESCRIPTION: Unwraps an IPv4-mapped IPv6 address
        OUTPUT: 10.20.30.40
        USE CASE: Collapsing duplicate client entries before counting blast radius

    .EXAMPLE
        PS> ConvertTo-KrbClientAddress -Value '::1'

        DESCRIPTION: Recognises a request made on the domain controller itself
        OUTPUT: ::1
        USE CASE: Excluding DC-local traffic from a client impact count

    .OUTPUTS
        System.String or $null

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/

        TROUBLESHOOTING:
        - Client attribution: .\Troubleshooting\Common\Client-Identification.md
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$Value
    )

    process {
        if ([string]::IsNullOrWhiteSpace($Value)) { return $null }

        $text = $Value.Trim()
        if ($text -eq '-') { return $null }

        # Only the ::ffff: form is unwrapped. The parser is deliberately narrow: a broader
        # "strip everything before the last colon" rule would mangle a genuine IPv6 address
        # into its final hextet.
        if ($text -match '^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$') {
            return $Matches[1]
        }

        return $text
    }
}