Private/ConvertTo-ScrubbedCliOutput.ps1

function ConvertTo-ScrubbedCliOutput {
    <#
    .SYNOPSIS
        Masks credential-shaped fragments in Azure CLI output before it is
        written to a log, thrown as an exception message, or bubbled back to
        the caller.
    .DESCRIPTION
        'az rest', 'az graph query', and 'az login' errors occasionally echo
        headers, body fragments, or command lines that contain bearer tokens,
        refresh tokens, client secrets, or passwords. Those strings must
        never land in log files or screen output.
 
        The scrubber replaces the secret value in each of these shapes with
        the literal '<redacted>' while keeping the surrounding key/field so
        the log remains diagnostically useful:
 
        - Bearer <jwt>
        - "access_token" / "refresh_token" / "id_token" / "password" /
          "client_secret" / "secret" / "authorization" : "..."
        - Standalone JWT tokens (three dot-separated base64url segments
          starting with 'eyJ').
        - CLI-argument forms: --password <v>, --client-secret <v>,
          --tenant-secret <v>, -p <v>.
        - HTTP header forms: Authorization: <v>
    .PARAMETER Text
        The CLI output to scrub. Null / empty input is returned unchanged.
    .OUTPUTS
        [string] with secret values replaced.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$Text
    )

    process {
        if ([string]::IsNullOrEmpty($Text)) { return $Text }

        $s = $Text

        # 1. Bearer <token>
        $s = [regex]::Replace($s, '(?i)(bearer\s+)[A-Za-z0-9\-_\.=]+', '$1<redacted>')

        # 2. JSON-style credential fields: "name":"value"
        $jsonKeys = '(?i)(\"(?:access_?token|refresh_?token|id_?token|password|client_?secret|clientSecret|secret|authorization|sas_?token|sasToken)\"\s*:\s*\")[^\"]*(\")'
        $s = [regex]::Replace($s, $jsonKeys, '$1<redacted>$2')

        # 3. Standalone JWTs (3 base64url segments, middle segment typically starts with eyJ)
        $s = [regex]::Replace($s, 'eyJ[A-Za-z0-9\-_]{8,}\.[A-Za-z0-9\-_]{8,}\.[A-Za-z0-9\-_]{8,}', '<redacted-jwt>')

        # 4. CLI argument forms: --password foo / -p foo
        $cliArgs = '(?i)(--(?:password|client-?secret|tenant-?secret|sas-?token|token|key)\s+)\S+'
        $s = [regex]::Replace($s, $cliArgs, '$1<redacted>')

        # 5. HTTP header form: Authorization: ...
        $s = [regex]::Replace($s, '(?im)^(\s*authorization\s*:\s*).*$', '$1<redacted>')

        return $s
    }
}