From 83c77703fae8cee939dc55e694a23e2df1381c2d Mon Sep 17 00:00:00 2001 From: Chisom92 Date: Thu, 26 Feb 2026 11:49:44 +0100 Subject: [PATCH] feat: Add PII and secret redaction to logging (#232) --- frontend/LOGGING_SECURITY.md | 42 ++++++++++++++++ frontend/src/lib/__tests__/logger.test.ts | 58 +++++++++++++++++++++ frontend/src/lib/logger.ts | 61 +++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 frontend/LOGGING_SECURITY.md create mode 100644 frontend/src/lib/__tests__/logger.test.ts create mode 100644 frontend/src/lib/logger.ts diff --git a/frontend/LOGGING_SECURITY.md b/frontend/LOGGING_SECURITY.md new file mode 100644 index 0000000..617c4c7 --- /dev/null +++ b/frontend/LOGGING_SECURITY.md @@ -0,0 +1,42 @@ +# Logging Security - Issue #232 + +## Implementation + +Added secure logging utility with automatic PII and secret redaction. + +### Redacted Data + +**Secrets:** +- Tokens (auth, bearer, API keys) +- Private keys +- Passwords +- Seeds/mnemonics + +**PII:** +- Email addresses (shows first 2 chars + domain) +- Wallet addresses (shows first 4 + last 4 chars) + +### Usage + +```typescript +import { logger } from '@/lib/logger'; + +// Safe logging - automatically redacts sensitive data +logger.info('User connected', { + address: 'GBRP...', + email: 'user@example.com' +}); + +logger.error('Auth failed', { token: 'abc123' }); +``` + +### Files Modified + +- `src/lib/logger.ts` - Secure logger utility +- `src/lib/__tests__/logger.test.ts` - Redaction tests + +### Testing + +Run tests: `npm test logger.test.ts` + +All sensitive fields are redacted before logging. diff --git a/frontend/src/lib/__tests__/logger.test.ts b/frontend/src/lib/__tests__/logger.test.ts new file mode 100644 index 0000000..71b57ac --- /dev/null +++ b/frontend/src/lib/__tests__/logger.test.ts @@ -0,0 +1,58 @@ +/** + * Tests for secure logger redaction + */ + +import { logger } from '../logger'; + +describe('Logger Redaction', () => { + let consoleLog: jest.SpyInstance; + let consoleError: jest.SpyInstance; + + beforeEach(() => { + consoleLog = jest.spyOn(console, 'log').mockImplementation(); + consoleError = jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + consoleLog.mockRestore(); + consoleError.mockRestore(); + }); + + it('redacts tokens', () => { + logger.info('Auth', { token: 'secret_token_12345678' }); + expect(consoleLog).toHaveBeenCalledWith('[INFO] Auth', expect.objectContaining({ + token: 'secr...5678' + })); + }); + + it('redacts wallet addresses', () => { + logger.info('Wallet', { address: 'GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H' }); + expect(consoleLog).toHaveBeenCalledWith('[INFO] Wallet', expect.objectContaining({ + address: 'GBRP...OX2H' + })); + }); + + it('redacts emails', () => { + logger.info('User', { email: 'user@example.com' }); + expect(consoleLog).toHaveBeenCalledWith('[INFO] User', expect.objectContaining({ + email: 'us***@example.com' + })); + }); + + it('redacts private keys', () => { + logger.error('Key error', { privateKey: 'SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' }); + expect(consoleError).toHaveBeenCalledWith('[ERROR] Key error', expect.objectContaining({ + privateKey: expect.stringContaining('...') + })); + }); + + it('redacts nested objects', () => { + logger.info('Nested', { user: { email: 'test@test.com', token: 'abc123xyz' } }); + expect(consoleLog).toHaveBeenCalledWith('[INFO] Nested', expect.objectContaining({ + user: expect.objectContaining({ + email: expect.stringContaining('***'), + token: expect.stringContaining('...') + }) + })); + }); +}); diff --git a/frontend/src/lib/logger.ts b/frontend/src/lib/logger.ts new file mode 100644 index 0000000..3854d11 --- /dev/null +++ b/frontend/src/lib/logger.ts @@ -0,0 +1,61 @@ +/** + * Secure logger with PII and secret redaction + */ + +const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'authorization', 'privateKey', 'seed', 'mnemonic']; +const ADDRESS_PATTERN = /G[A-Z0-9]{55}/g; +const EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; + +function redactValue(key: string, value: unknown): unknown { + if (typeof value === 'string') { + // Redact auth tokens and keys + if (SENSITIVE_KEYS.some(k => key.toLowerCase().includes(k))) { + return value.length > 8 ? `${value.slice(0, 4)}...${value.slice(-4)}` : '[REDACTED]'; + } + // Redact wallet addresses + if (key.toLowerCase().includes('address') || key.toLowerCase().includes('wallet')) { + return value.replace(ADDRESS_PATTERN, (addr) => `${addr.slice(0, 4)}...${addr.slice(-4)}`); + } + // Redact emails + if (key.toLowerCase().includes('email')) { + return value.replace(EMAIL_PATTERN, (email) => { + const [local, domain] = email.split('@'); + return `${local.slice(0, 2)}***@${domain}`; + }); + } + } + return value; +} + +function redactObject(obj: unknown): unknown { + if (!obj || typeof obj !== 'object') return obj; + + if (Array.isArray(obj)) { + return obj.map(item => redactObject(item)); + } + + const redacted: Record = {}; + for (const [key, value] of Object.entries(obj)) { + redacted[key] = typeof value === 'object' ? redactObject(value) : redactValue(key, value); + } + return redacted; +} + +export const logger = { + info: (message: string, data?: unknown) => { + if (process.env.NODE_ENV !== 'production') { + console.log(`[INFO] ${message}`, data ? redactObject(data) : ''); + } + }, + + error: (message: string, error?: unknown) => { + const redacted = error instanceof Error + ? { message: error.message, name: error.name } + : redactObject(error); + console.error(`[ERROR] ${message}`, redacted); + }, + + warn: (message: string, data?: unknown) => { + console.warn(`[WARN] ${message}`, data ? redactObject(data) : ''); + } +};