Extract dashboard redaction helpers
Centralize dashboard secret redaction helpers and add focused regression tests.
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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) + '…';
|
||||||
}
|
}
|
||||||
|
|||||||
26
apps/dashboard/src/redaction.test.ts
Normal file
26
apps/dashboard/src/redaction.test.ts
Normal 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>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
apps/dashboard/src/redaction.ts
Normal file
17
apps/dashboard/src/redaction.ts
Normal 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>');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user