diff --git a/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts b/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts index 36dc12ac85..d69add6fcb 100644 --- a/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts +++ b/apps/desktop/src/main/__tests__/rive-workflow-tool.test.ts @@ -24,6 +24,8 @@ import { join } from 'node:path'; import { describe, it } from 'node:test'; import { buildRiveCommand, + redactRiveText, + redactRiveValue, runRiveCli, RiveCliError, } from '../rive-cli.js'; @@ -243,6 +245,26 @@ describe('RiveWorkflow tool and CLI bridge', { concurrency: false }, () => { }); }); + it('uses the core redaction coverage for token forms and sensitive keys', () => { + const text = redactRiveText( + 'ghp_12345678901234567890 AIza12345678901234567890 xoxb-1234567890 Bearer opaque-session-token', + ); + assert.equal(text.includes('ghp_12345678901234567890'), false); + assert.equal(text.includes('AIza12345678901234567890'), false); + assert.equal(text.includes('xoxb-1234567890'), false); + assert.equal(text.includes('opaque-session-token'), false); + assert.match(text, /Bearer \[redacted\]/); + + const value = redactRiveValue({ + apiKey: 'plain-value', + nested: [{ authorization: 'Bearer plain-value' }], + }); + assert.deepEqual(value, { + apiKey: '[redacted]', + nested: [{ authorization: '[redacted]' }], + }); + }); + }); async function runTool( diff --git a/apps/desktop/src/main/rive-cli.ts b/apps/desktop/src/main/rive-cli.ts index 0e5f7189c2..146636cf47 100644 --- a/apps/desktop/src/main/rive-cli.ts +++ b/apps/desktop/src/main/rive-cli.ts @@ -20,6 +20,7 @@ import { access } from 'node:fs/promises'; import { constants } from 'node:fs'; import { spawn } from 'node:child_process'; +import { isSensitiveKey, redactSecrets } from '@maka/core/redaction'; export type RiveCliAction = | 'workflow_validate' @@ -177,11 +178,7 @@ export async function runRiveCli(input: RiveCliToolArgs, options: RiveCliRunOpti } export function redactRiveText(input: string): string { - return input - .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]+=*/gi, '$1[REDACTED]') - .replace(/\b(sk-[A-Za-z0-9][A-Za-z0-9_-]{8,})\b/g, '[REDACTED]') - .replace(/\b((?:api[_-]?key|token|secret|password)\s*[:=]\s*)("[^"]+"|'[^']+'|[^\s,;]+)/gi, '$1[REDACTED]') - .replace(/\b([A-Za-z0-9_-]*(?:token|secret|password|api[_-]?key)[A-Za-z0-9_-]*\s*[:=]\s*)("[^"]+"|'[^']+'|[^\s,;]+)/gi, '$1[REDACTED]'); + return redactSecrets(input); } export function redactRiveValue(value: unknown, depth = 0): unknown { @@ -191,7 +188,7 @@ export function redactRiveValue(value: unknown, depth = 0): unknown { if (!value || typeof value !== 'object') return value; const out: Record = {}; for (const [key, item] of Object.entries(value)) { - out[key] = redactRiveValue(item, depth + 1); + out[key] = isSensitiveKey(key) ? '[redacted]' : redactRiveValue(item, depth + 1); } return out; } diff --git a/packages/core/src/__tests__/redaction.test.ts b/packages/core/src/__tests__/redaction.test.ts index 6689071875..bf6c6d0931 100644 --- a/packages/core/src/__tests__/redaction.test.ts +++ b/packages/core/src/__tests__/redaction.test.ts @@ -55,6 +55,13 @@ describe('redactSecrets', () => { assert.match(inspected, /proxyAuthorization: 'Token \[redacted\]'/); }); + test('masks standalone bearer values', () => { + const text = redactSecrets('prefix Bearer opaque-session-token suffix'); + + assert.equal(text, 'prefix Bearer [redacted] suffix'); + assert.equal(text.includes('opaque-session-token'), false); + }); + test('applies bounded text patterns to top-level JSON number primitives', () => { assert.equal(redactSecrets('1234567890123456789012345678901234567890'), '[redacted]'); }); diff --git a/packages/core/src/redaction.ts b/packages/core/src/redaction.ts index 0144baead4..c0a21f2ac9 100644 --- a/packages/core/src/redaction.ts +++ b/packages/core/src/redaction.ts @@ -46,6 +46,7 @@ const ASSIGNED_SECRET_KEY_VALUE_PATTERN = /\b(([A-Za-z][A-Za-z0-9_-]*)(?:[ \t]|\\\r?\n)*[:=](?:[ \t]|\\\r?\n)*['"]?)(?:\\\r?\n|[^\s"'&<>])+/g; const AUTHORIZATION_HEADER_PATTERN = /(^|[^A-Za-z0-9_])(['"]?(?:proxy[-_]?authorization|authorization)['"]?\s*:\s*['"]?(?:bearer|basic|token)\s+)[^\s"'<>]+/gim; +const STANDALONE_BEARER_PATTERN = /\b(Bearer\s+)[A-Za-z0-9._~+/-]+=*/gi; const AWS_CLI_SPACE_SECRET_PATTERN = new RegExp( `(^|[\\s;&|()])((?:aws${SHELL_SEPARATOR_SOURCE}configure${SHELL_SEPARATOR_SOURCE}set${SHELL_SEPARATOR_SOURCE}${AWS_CONFIG_SECRET_KEY_SOURCE}|${AWS_SECRET_ACCESS_KEY_FLAG_SOURCE})${SHELL_SEPARATOR_SOURCE})${SHELL_SECRET_TOKEN_SOURCE}`, 'gm', @@ -78,6 +79,7 @@ function redactTextSecrets(value: string): string { AUTHORIZATION_HEADER_PATTERN, (_match, boundary: string, prefix: string) => `${boundary}${prefix}[redacted]`, ); + next = next.replace(STANDALONE_BEARER_PATTERN, (_match, prefix: string) => `${prefix}[redacted]`); next = next.replace( AWS_CLI_SPACE_SECRET_PATTERN, (_match, boundary: string, prefix: string, token: string) =>