Public/Invoke-JsonSanitize.ps1

function Invoke-JsonSanitize {
    <#
    .SYNOPSIS
        Sanitize sensitive values from a JSON payload before sharing it with AI tools.
 
    .DESCRIPTION
        Invoke-JsonSanitize parses a JSON string or file, detects sensitive values
        (API keys, tokens, email addresses, IP addresses, phone numbers, SSNs, credit
        card numbers, credentials in URLs, and hostnames), replaces each with a
        realistic-looking fake equivalent, and returns valid JSON with the original
        structure intact.
 
        Everything runs locally. No data is sent over the network.
 
    .PARAMETER InputObject
        A JSON string to sanitize. Accepts pipeline input.
 
    .PARAMETER Path
        Path to a JSON file to read and sanitize.
 
    .PARAMETER DryRun
        Display a table of detected values and their proposed replacements without
        modifying anything. Nothing is written to stdout or OutFile.
 
    .PARAMETER OutFile
        Write the sanitized JSON to this file path instead of stdout.
 
    .EXAMPLE
        Invoke-JsonSanitize -Path ./response.json
 
        Reads response.json, sanitizes it, and prints the result to stdout.
 
    .EXAMPLE
        Get-Content ./payload.json -Raw | Invoke-JsonSanitize
 
        Pipes raw JSON text through the sanitizer and prints the result to stdout.
 
    .EXAMPLE
        Invoke-JsonSanitize -Path ./payload.json -DryRun
 
        Shows a preview table of every replacement that would be made. No changes applied.
 
    .EXAMPLE
        Invoke-JsonSanitize -Path ./payload.json -OutFile ./payload.sanitized.json
 
        Writes the sanitized payload to a new file instead of printing to stdout.
 
    .EXAMPLE
        $json = '{"email":"alice@example.com","apiKey":"sk-abc123def456ghi789jkl0"}' | Invoke-JsonSanitize
 
        Sanitizes a JSON string stored in a variable.
 
    .INPUTS
        System.String
 
    .OUTPUTS
        System.String. Sanitized JSON (unless -DryRun is specified, in which case
        nothing is written to the output stream).
 
    .NOTES
        Install from PSGallery:
            Install-Module PeachSanitize
            Import-Module PeachSanitize
 
        Detection covers: email, API keys/tokens (Shannon entropy), OAuth/JWT tokens,
        Bearer tokens, IPv4 addresses, URLs with embedded credentials, US phone numbers,
        SSNs, credit card numbers (Luhn-validated), hostnames/FQDNs, and any value
        whose key name contains a sensitive keyword (password, secret, token, key, etc.).
 
        This is the manual, local equivalent of what Peach Security does automatically
        at the browser layer across all your clients. Learn more at peachsecurity.io.
    #>

    [CmdletBinding(DefaultParameterSetName = 'String', SupportsShouldProcess)]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, ParameterSetName = 'String', ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string] $InputObject,

        [Parameter(Mandatory, ParameterSetName = 'File')]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter()]
        [switch] $DryRun,

        [Parameter()]
        [string] $OutFile
    )

    begin {
        $jsonChunks = [System.Collections.Generic.List[string]]::new()
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'String') {
            $jsonChunks.Add($InputObject)
        }
    }

    end {
        # Resolve the raw JSON string from a file or the accumulated pipeline input.
        if ($PSCmdlet.ParameterSetName -eq 'File') {
            $fullPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)

            if (-not [System.IO.File]::Exists($fullPath)) {
                $ex = [System.IO.FileNotFoundException]::new("File not found: '$fullPath'.")
                $PSCmdlet.ThrowTerminatingError(
                    [System.Management.Automation.ErrorRecord]::new(
                        $ex, 'FileNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $fullPath))
            }

            if (([System.IO.FileInfo]::new($fullPath)).Length -gt 1MB) {
                $PSCmdlet.WriteWarning('Large file detected - processing may be slow.')
            }

            $rawJson = [System.IO.File]::ReadAllText($fullPath, [System.Text.Encoding]::UTF8)
        }
        else {
            $rawJson = $jsonChunks -join ''
        }

        # Validate JSON.
        try {
            $parsed = $rawJson | ConvertFrom-Json
        }
        catch {
            # Deliberately omit the raw payload from the error record: it may contain
            # the very secrets this tool exists to protect, and ErrorRecords can be logged.
            $ex = [System.ArgumentException]::new(
                'Input is not valid JSON. Verify with ConvertFrom-Json before running.')
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    $ex, 'InvalidJson', [System.Management.Automation.ErrorCategory]::InvalidData, $null))
        }

        # Walk the object tree and replace sensitive leaf values.
        $walkResult = Invoke-ValueReplacement -InputObject $parsed -KeyPath ''
        $findings   = $walkResult.Findings
        $sanitized  = $walkResult.Object

        Write-Verbose ('{0} sensitive value(s) detected across {1} type(s).' -f
            $findings.Count,
            ($findings | Select-Object -ExpandProperty DetectedType -Unique | Measure-Object).Count)

        # DryRun: render a preview table to the host only, change nothing.
        if ($DryRun) {
            if ($findings.Count -eq 0) {
                'No sensitive values detected.' | Out-Host
                return
            }
            $findings |
                Format-Table -Property KeyPath, DetectedType, OriginalValue, ProposedReplacement -AutoSize |
                Out-Host
            return
        }

        $outputJson = $sanitized | ConvertTo-Json -Depth 100

        if (-not $OutFile) {
            return $outputJson
        }

        if ($PSCmdlet.ShouldProcess($OutFile, 'Write sanitized JSON')) {
            $outResolved = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutFile)
            try {
                [System.IO.File]::WriteAllText($outResolved, $outputJson, [System.Text.Encoding]::UTF8)
                Write-Verbose "Sanitized JSON written to: $outResolved"
            }
            catch [System.UnauthorizedAccessException] {
                $ex = [System.UnauthorizedAccessException]::new("Cannot write to '$outResolved' - access denied.")
                $PSCmdlet.ThrowTerminatingError(
                    [System.Management.Automation.ErrorRecord]::new(
                        $ex, 'OutFileAccessDenied', [System.Management.Automation.ErrorCategory]::PermissionDenied, $outResolved))
            }
            catch [System.IO.IOException] {
                $ex = [System.IO.IOException]::new(
                    "Failed to write output file '$outResolved': $($_.Exception.Message)")
                $PSCmdlet.ThrowTerminatingError(
                    [System.Management.Automation.ErrorRecord]::new(
                        $ex, 'OutFileWriteError', [System.Management.Automation.ErrorCategory]::WriteError, $outResolved))
            }
        }
    }
}