Extract dashboard redaction helpers

Centralize dashboard secret redaction helpers and add focused regression tests.
This commit is contained in:
Eyejoker
2026-04-28 13:25:46 +09:00
committed by GitHub
parent 23c5be6ecd
commit f162f42c6b
4 changed files with 47 additions and 15 deletions

View File

@@ -61,6 +61,7 @@ import {
isWatcherRoomMessage, isWatcherRoomMessage,
} from './roomThread'; } from './roomThread';
import { ParsedBody } from './ParsedBody'; import { ParsedBody } from './ParsedBody';
import { redactSecretsForPreview } from './redaction';
import './styles.css'; import './styles.css';
interface DashboardState { interface DashboardState {
@@ -439,18 +440,11 @@ function queueLabel(
return parts.join(' · '); return parts.join(' · ');
} }
const SECRET_ASSIGNMENT_RE =
/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi;
const SECRET_VALUE_RE =
/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g;
function safePreview( function safePreview(
value: string | null | undefined, value: string | null | undefined,
fallback: string, fallback: string,
): string { ): string {
const cleaned = (value ?? '') const cleaned = redactSecretsForPreview(value ?? '')
.replace(SECRET_ASSIGNMENT_RE, '$1=<redacted>')
.replace(SECRET_VALUE_RE, '<redacted-token>')
.replace(/<\/?internal[^>]*>/gi, '') .replace(/<\/?internal[^>]*>/gi, '')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim(); .trim();

View File

@@ -2,10 +2,7 @@ import { useMemo, type ReactNode } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
const SECRET_ASSIGNMENT_RE = import { redactSecretsForMarkdown } from './redaction';
/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi;
const SECRET_VALUE_RE =
/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g;
export function ParsedBody({ export function ParsedBody({
text, text,
@@ -16,9 +13,7 @@ export function ParsedBody({
}): ReactNode { }): ReactNode {
const cleaned = useMemo(() => { const cleaned = useMemo(() => {
if (!text) return ''; if (!text) return '';
let out = text let out = redactSecretsForMarkdown(text);
.replace(SECRET_ASSIGNMENT_RE, (_m, key) => `${key}=***`)
.replace(SECRET_VALUE_RE, '***');
if (truncate && out.length > truncate) { if (truncate && out.length > truncate) {
out = out.slice(0, truncate) + '…'; out = out.slice(0, truncate) + '…';
} }

View File

@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { redactSecretsForMarkdown, redactSecretsForPreview } from './redaction';
describe('dashboard redaction', () => {
it('redacts named secret assignments for markdown output', () => {
expect(redactSecretsForMarkdown('OPENAI_API_KEY=sk-testvalue1234')).toBe(
'OPENAI_API_KEY=***',
);
});
it('redacts named secret assignments for compact previews', () => {
expect(redactSecretsForPreview('BOT_TOKEN=xoxb-testvalue1234')).toBe(
'BOT_TOKEN=<redacted>',
);
});
it('redacts standalone secret values consistently', () => {
const raw = 'token sk-1234567890abcdef and ghp_1234567890abcdef';
expect(redactSecretsForMarkdown(raw)).toBe('token *** and ***');
expect(redactSecretsForPreview(raw)).toBe(
'token <redacted-token> and <redacted-token>',
);
});
});

View File

@@ -0,0 +1,17 @@
export const SECRET_ASSIGNMENT_RE =
/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi;
export const SECRET_VALUE_RE =
/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g;
export function redactSecretsForMarkdown(value: string): string {
return value
.replace(SECRET_ASSIGNMENT_RE, (_m, key) => `${key}=***`)
.replace(SECRET_VALUE_RE, '***');
}
export function redactSecretsForPreview(value: string): string {
return value
.replace(SECRET_ASSIGNMENT_RE, '$1=<redacted>')
.replace(SECRET_VALUE_RE, '<redacted-token>');
}