Private/ConvertTo-FlatObject.ps1

function ConvertTo-FlatObject {
    <#
    .SYNOPSIS
    Flattens a nested PSObject (audit record) into a single-level hashtable.
 
    .DESCRIPTION
    Extracted from Search-UnifiedAuditLogCustom so the flattening logic is
    reusable and unit-testable on its own. Recursively expands nested objects,
    dictionaries and lists into prefixed keys, and special-cases the audit
    Parameters array (ParameterString / Param_* / FullCommand).
    #>

    param (
        [Parameter(Mandatory = $true)]
        [PSObject]$InputObject,

        [Parameter(Mandatory = $false)]
        [string]$Prefix = '',

        [Parameter(Mandatory = $false)]
        [switch]$PreserveTypes
    )

    $flatProperties = @{}

    foreach ($property in $InputObject.PSObject.Properties) {
        $key = if ($Prefix) { "${Prefix}_$($property.Name)" } else { $property.Name }

        if ($property.Name -eq 'Parameters' -and $property.Value -is [Array]) {
            $parameterStrings = foreach ($parameter in $property.Value) {
                "$($parameter.Name)=$($parameter.Value)"
            }
            $flatProperties['ParameterString'] = $parameterStrings -join ' | '

            foreach ($parameter in $property.Value) {
                $parameterKey = "Param_$($parameter.Name)"
                $flatProperties[$parameterKey] = $parameter.Value
            }

            if ($InputObject.Operation) {
                $parameterStrings = foreach ($parameter in $property.Value) {
                    $parameterValue = switch -Regex ($parameter.Value) {
                        '\s' { "'$($parameter.Value)'" }
                        '^True$|^False$' { "`$$($parameter.Value.ToLower())" }
                        ';' { "'$($parameter.Value)'" }
                        default { $parameter.Value }
                    }
                    "-$($parameter.Name) $parameterValue"
                }
                $flatProperties['FullCommand'] = "$($InputObject.Operation) $($parameterStrings -join ' ')"
            }

            continue
        }

        switch ($property.Value) {
            { $_ -is [System.Collections.IDictionary] } {
                $nestedProperties = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes
                foreach ($nestedKey in $nestedProperties.Keys) {
                    $uniqueKey = if ($flatProperties.ContainsKey($nestedKey)) {
                        $counter = 1
                        while ($flatProperties.ContainsKey("${nestedKey}_$counter")) {
                            $counter++
                        }
                        "${nestedKey}_$counter"
                    }
                    else {
                        $nestedKey
                    }
                    $flatProperties[$uniqueKey] = $nestedProperties[$nestedKey]
                }
            }
            { $_ -is [System.Collections.IList] -and $property.Name -ne 'Parameters' } {
                if ($_.Count -gt 0) {
                    if ($_[0] -is [PSObject]) {
                        # If every item has a recipient-like shape (Address/EmailAddress/SmtpAddress
                        # +/- Name), collapse the array into a single readable pipe-joined string
                        # instead of exploding it into Recipients_0_Address, Recipients_0_Name, ...
                        $addressProp = $null
                        foreach ($candidate in @('Address', 'EmailAddress', 'SmtpAddress')) {
                            if ($_[0].PSObject.Properties.Name -contains $candidate) {
                                $addressProp = $candidate
                                break
                            }
                        }
                        $everyItemHasAddress = $false
                        if ($addressProp) {
                            $everyItemHasAddress = -not ($_ | Where-Object { -not ($_.PSObject.Properties.Name -contains $addressProp) } | Select-Object -First 1)
                        }

                        if ($everyItemHasAddress) {
                            $joined = ($_ | ForEach-Object {
                                    $addr = $_.$addressProp
                                    $name = if ($_.PSObject.Properties.Name -contains 'Name') { $_.Name } else { $null }
                                    if ($name -and ($name -ne $addr)) { "$name <$addr>" } else { $addr }
                                }) -join '|'
                            $flatProperties[$key] = $joined
                        }
                        else {
                            for ($i = 0; $i -lt $_.Count; $i++) {
                                $nestedProperties = ConvertTo-FlatObject -InputObject $_[$i] -Prefix "${key}_${i}" -PreserveTypes:$PreserveTypes
                                foreach ($nestedKey in $nestedProperties.Keys) {
                                    $uniqueKey = if ($flatProperties.ContainsKey($nestedKey)) {
                                        $counter = 1
                                        while ($flatProperties.ContainsKey("${nestedKey}_$counter")) {
                                            $counter++
                                        }
                                        "${nestedKey}_$counter"
                                    }
                                    else {
                                        $nestedKey
                                    }
                                    $flatProperties[$uniqueKey] = $nestedProperties[$nestedKey]
                                }
                            }
                        }
                    }
                    else {
                        $flatProperties[$key] = $_ -join '|'
                    }
                }
                else {
                    $flatProperties[$key] = [string]::Empty
                }
            }
            { $_ -is [PSObject] } {
                $nestedProperties = ConvertTo-FlatObject -InputObject $_ -Prefix $key -PreserveTypes:$PreserveTypes
                foreach ($nestedKey in $nestedProperties.Keys) {
                    $uniqueKey = if ($flatProperties.ContainsKey($nestedKey)) {
                        $counter = 1
                        while ($flatProperties.ContainsKey("${nestedKey}_$counter")) {
                            $counter++
                        }
                        "${nestedKey}_$counter"
                    }
                    else {
                        $nestedKey
                    }
                    $flatProperties[$uniqueKey] = $nestedProperties[$nestedKey]
                }
            }
            default {
                if ($PreserveTypes) {
                    $flatProperties[$key] = $_
                }
                else {
                    $flatProperties[$key] = switch ($_) {
                        { $_ -is [datetime] } { $_ }
                        { $_ -is [bool] } { $_ }
                        { $_ -is [int] } { $_ }
                        { $_ -is [long] } { $_ }
                        { $_ -is [decimal] } { $_ }
                        { $_ -is [double] } { $_ }
                        default { [string]$_ }
                    }
                }
            }
        }
    }

    return $flatProperties
}