export const REDACTED_COMMAND_TEXT_VALUE = "***REDACTED***"; const SECRET_NAME_PATTERN = String.raw`[A-Za-z0-9_-]*(?:api[-_]?key|(?:access[-_]?|auth[-_]?)?token|token|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)[A-Za-z0-9_-]*`; const COMMAND_CLI_SECRET_OPTION_RE = new RegExp( String.raw`(\B-{1,2}${SECRET_NAME_PATTERN}(?:\s+|=)(["']?))[^\s"'` + "`" + String.raw`]+(\2)`, "gi", ); const COMMAND_ENV_SECRET_ASSIGNMENT_RE = new RegExp( String.raw`(\b${SECRET_NAME_PATTERN}\s*=\s*)(?:(\\["'])([\s\S]*?)\2|(["'])([^"'` + "`" + String.raw`\r\n]*)\4|([^\s"'` + "`" + String.raw`]+))`, "gi", ); const COMMAND_AUTHORIZATION_BEARER_RE = /(\bAuthorization\s*:\s*Bearer\s+)[^\s"'`]+/gi; const COMMAND_OPENAI_KEY_RE = /\bsk-[A-Za-z0-9_-]{12,}\b/g; const COMMAND_GITHUB_TOKEN_RE = /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g; const COMMAND_JWT_RE = /\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]{8,})?\b/g; const COMMAND_SECRET_HINTS = [ "api", "key", "token", "auth", "bearer", "secret", "pass", "credential", "jwt", "private", "cookie", "connectionstring", "sk-", "ghp_", "gho_", "ghu_", "ghs_", "ghr_", ] as const; function maybeContainsSecretText(command: string) { const lower = command.toLowerCase(); return ( COMMAND_SECRET_HINTS.some((hint) => lower.includes(hint)) || command.includes(".") ); } export function redactCommandText( command: string, redactedValue = REDACTED_COMMAND_TEXT_VALUE, ): string { if (!maybeContainsSecretText(command)) return command; return command .replace(COMMAND_AUTHORIZATION_BEARER_RE, `$1${redactedValue}`) .replace(COMMAND_CLI_SECRET_OPTION_RE, `$1${redactedValue}$3`) .replace( COMMAND_ENV_SECRET_ASSIGNMENT_RE, ( _match, prefix: string, escapedQuote: string | undefined, _escapedValue: string | undefined, rawQuote: string | undefined, ) => { const quote = escapedQuote ?? rawQuote; return quote ? `${prefix}${quote}${redactedValue}${quote}` : `${prefix}${redactedValue}`; }, ) .replace(COMMAND_OPENAI_KEY_RE, redactedValue) .replace(COMMAND_GITHUB_TOKEN_RE, redactedValue) .replace(COMMAND_JWT_RE, redactedValue); } // A JSON secret field is a key/value pair such as `"token":"opaque-value"`. The // command redaction handles shell `KEY=value` syntax only. A sandbox diagnostic // can also carry a serialized JSON error, so the sanitizer must redact the JSON // form too. The value body consumes JSON escape sequences. An escaped quote // (`\"`) inside the value does not end the match early. const JSON_SECRET_FIELD_RE = new RegExp( String.raw`("(?:${SECRET_NAME_PATTERN})"\s*:\s*")(?:\\[\s\S]|[^"\\])*(")`, "gi", ); // An escaped JSON secret field is the same pair inside a JSON string. The double // quote appears as `\"` and a backslash appears as `\\`. The value body // consumes the doubled escape sequences. An escaped quote inside the value does // not end the match early. The value ends at the next unescaped `\"`. const JSON_ESCAPED_SECRET_FIELD_RE = new RegExp( String.raw`(\\"(?:${SECRET_NAME_PATTERN})\\"\s*:\s*\\")(?:\\\\\\\\|\\\\\\"|\\\\[\s\S]|[^\\"])*(\\")`, "gi", ); /** * Redact secrets from an untrusted diagnostic string. * * The function first runs the command redaction. The command redaction handles * shell `KEY=value` assignments, CLI secret options, bearer headers, and common * token shapes. The function then redacts JSON and escaped-JSON secret fields, * because a sandbox diagnostic can carry a serialized JSON error such as * `{"token":"opaque-value"}`. The caller must still bound the length after this * step. */ export function redactDiagnosticText( text: string, redactedValue = REDACTED_COMMAND_TEXT_VALUE, ): string { return redactCommandText(text, redactedValue) .replace(JSON_ESCAPED_SECRET_FIELD_RE, `$1${redactedValue}$2`) .replace(JSON_SECRET_FIELD_RE, `$1${redactedValue}$2`); }