Private/Protect-TLSnapshot.ps1

function Get-TLPseudonym {
    <#
    .SYNOPSIS
        Deterministic pseudonym for a user-identifying value (e.g. user-a3f2c1d0).
    .DESCRIPTION
        SHA-256 of the lower-cased value, first 8 hex chars. Deterministic, so
        diffs across redacted snapshots still line up.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Value,

        [string]$Prefix = 'user'
    )

    $sha256 = [System.Security.Cryptography.SHA256]::Create()
    try {
        $hash = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value.ToLowerInvariant()))
    }
    finally {
        $sha256.Dispose()
    }
    $hex = -join (@($hash[0..3]) | ForEach-Object { $_.ToString('x2') })
    return ('{0}-{1}' -f $Prefix, $hex)
}

function Protect-TLSnapshot {
    <#
    .SYNOPSIS
        Returns a copy of the data tree with user-identifying values pseudonymized.
    .DESCRIPTION
        Redacts UPNs, mail addresses and user display names deterministically.
        GUIDs, policy names and configuration values stay untouched, so the
        snapshot remains fully assessable and diffable.
    #>

    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$InputObject
    )

    $redactKeys = @(
        'userPrincipalName', 'mail', 'otherMails', 'mailNickname', 'givenName',
        'surname', 'onPremisesSamAccountName', 'imAddresses', 'proxyAddresses',
        'userDisplayName', '_tlPrincipalUpn', '_tlPrincipalName'
    )
    $userListKeys = @('usersInclude', 'usersExclude')
    $wellKnownTokens = @('All', 'None', 'GuestsOrExternalUsers')
    $mailPattern = '^[^@\s]+@[^@\s]+\.[^@\s]+$'
    $guidPattern = '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$'

    function Protect-TLValue {
        param(
            [AllowNull()][object]$Value,
            [AllowEmptyString()][string]$Key = '',
            [bool]$IsUserObject = $false
        )

        if ($null -eq $Value) { return $null }

        if ($Value -is [string]) {
            if ([string]::IsNullOrEmpty($Value)) { return $Value }
            if ($redactKeys -contains $Key) { return (Get-TLPseudonym -Value $Value) }
            if ($Key -eq 'displayName' -and $IsUserObject) { return (Get-TLPseudonym -Value $Value) }
            if ($userListKeys -contains $Key -and ($wellKnownTokens -notcontains $Value) -and ($Value -notmatch $guidPattern)) {
                return (Get-TLPseudonym -Value $Value)
            }
            if ($Value -match $mailPattern) { return (Get-TLPseudonym -Value $Value) }
            return $Value
        }

        if ($Value.GetType().IsValueType) { return $Value }

        if ($Value -is [System.Collections.IDictionary]) {
            $isUser = $false
            if ($Value.Contains('userPrincipalName')) { $isUser = $true }
            elseif ($Value.Contains('@odata.type') -and ([string]$Value['@odata.type']) -match '(?i)\.user$') { $isUser = $true }
            $copy = [ordered]@{}
            foreach ($entryKey in @($Value.Keys)) {
                $copy[[string]$entryKey] = Protect-TLValue -Value $Value[$entryKey] -Key ([string]$entryKey) -IsUserObject $isUser
            }
            return $copy
        }

        if ($Value -is [System.Collections.IEnumerable]) {
            $items = @(foreach ($item in $Value) {
                    # scalar lists inherit the key context (otherMails, usersExclude, ...)
                    Protect-TLValue -Value $item -Key $Key -IsUserObject $IsUserObject
                })
            return , $items
        }

        # PSCustomObject
        $isUser = $false
        if ($Value.PSObject.Properties['userPrincipalName']) { $isUser = $true }
        elseif ($Value.PSObject.Properties['@odata.type'] -and ([string]$Value.'@odata.type') -match '(?i)\.user$') { $isUser = $true }
        $copy = [ordered]@{}
        foreach ($property in $Value.PSObject.Properties) {
            $copy[$property.Name] = Protect-TLValue -Value $property.Value -Key $property.Name -IsUserObject $isUser
        }
        return $copy
    }

    return (Protect-TLValue -Value $InputObject)
}