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', '_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) } |