feat: add secret redaction utility

This commit is contained in:
2026-06-11 07:54:00 +01:00
parent d7e9d1122a
commit 5119c7b00c
4 changed files with 99 additions and 3 deletions
+3 -2
View File
@@ -11,14 +11,15 @@ Phase 0 complete. Phase 1 in progress.
## Current Phase ## Current Phase
Phase 0 - Repository Setup Phase 1 - Core Utilities
## Completed Tasks ## Completed Tasks
- Task 0.1 — Create repository skeleton ✅ - Task 0.1 — Create repository skeleton ✅
- Task 0.2 — package.json with dependencies ✅ - Task 0.2 — package.json with dependencies ✅
- Task 1.1 — Configuration loader ✅ - Task 1.1 — Configuration loader ✅
- Task 1.2 — Secret redaction utility (`src/utils/redact.js`) ✅
## Next Task ## Next Task
Task 1.2Secret redaction utility (`src/utils/redact.js`) Task 1.3Context budget utility (`src/utils/context-budget.js`)
+1 -1
View File
@@ -60,7 +60,7 @@ Requirements:
- Replace detected secrets with `[REDACTED]`. - Replace detected secrets with `[REDACTED]`.
- Return the redacted string. - Return the redacted string.
Status: ⏳ Pending Status: ✅ Complete
### Task 1.3 - Context budget utility ### Task 1.3 - Context budget utility
+28
View File
@@ -1 +1,29 @@
// Secret redaction utility. // Secret redaction utility.
// Detects common secret patterns and replaces values with [REDACTED].
export function redact(input) {
const str = typeof input === 'string' ? input : String(input);
let out = str;
// PEM blocks (multi-line): replace content between BEGIN/END markers
out = out.replace(
/(-----BEGIN[\s\S]*?PRIVATE KEY-----)[\s\S]*?(-----END[\s\S]*?PRIVATE KEY-----)/,
'$1\n[REDACTED]\n$2'
);
// Bearer tokens: preserve "Bearer " prefix
out = out.replace(/(Bearer\s+)(["']?\w[\w.-]+["']?)/g, '$1[REDACTED]');
// Git credentials: https://user:password@host → https://user:[REDACTED]@host
out = out.replace(/(https:\/\/\w+:)\S+(?=@)/, '$1[REDACTED]');
// Key=value secrets — quoted (double or single): capture up to matching close quote
const Q_PATTERN = /((?:OPENAI_API_KEY|API_KEY|TOKEN|SECRET|PASSWORD)[_\w]*)[ ]*=[ ]*(["'])(.*?)\2/gi;
out = out.replace(Q_PATTERN, '$1=$2[REDACTED]$2');
// Key=value secrets — unquoted: capture non-whitespace value
const U_PATTERN = /((?:OPENAI_API_KEY|API_KEY|TOKEN|SECRET|PASSWORD)[_\w]*)[ ]*=(?![\0`"'])([\S]+)/gi;
out = out.replace(U_PATTERN, '$1=[REDACTED]');
return out;
}
+67
View File
@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest';
import { redact } from '../../src/utils/redact.js';
describe('redact', () => {
it('redacts OPENAI_API_KEY (quoted)', () => {
const input = 'OPENAI_API_KEY="abc123"';
const output = redact(input);
expect(output).toBe('OPENAI_API_KEY="[REDACTED]"');
});
it('redacts API_KEY (unquoted)', () => {
const input = 'API_KEY=secretvalue';
const output = redact(input);
expect(output).toBe('API_KEY=[REDACTED]');
});
it('redacts TOKEN with AUTH_ prefix', () => {
const input = 'AUTH_TOKEN="my-token"';
const output = redact(input);
expect(output).toBe('AUTH_TOKEN="[REDACTED]"');
});
it('redacts SECRET value (single quotes preserved)', () => {
const input = "SECRET='s3cret'";
const output = redact(input);
expect(output).toBe("SECRET='[REDACTED]'");
});
it('redacts PASSWORD (unquoted)', () => {
const input = 'PASSWORD=plaintext';
const output = redact(input);
expect(output).toBe('PASSWORD=[REDACTED]');
});
it('redacts Bearer token', () => {
const input = 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9';
const output = redact(input);
expect(output).toBe('Authorization: Bearer [REDACTED]');
});
it('redacts PEM private key block (multi-line)', () => {
const input = '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA\n-----END RSA PRIVATE KEY-----';
const output = redact(input);
expect(output).toBe('-----BEGIN RSA PRIVATE KEY-----\n[REDACTED]\n-----END RSA PRIVATE KEY-----');
});
it('redacts Git URL credentials (user:password format)', () => {
const input = 'https://deploy:user:p4ss@github.com/org/repo.git';
const output = redact(input);
expect(output).toBe('https://deploy:[REDACTED]@github.com/org/repo.git');
});
it('handles multiple secrets in one string', () => {
const input = 'OPENAI_API_KEY="abc" TOKEN=\'def\' PASSWORD=ghi';
const output = redact(input);
expect(output).toBe('OPENAI_API_KEY="[REDACTED]" TOKEN=\'[REDACTED]\' PASSWORD=[REDACTED]');
});
it('returns unchanged input when no secrets found', () => {
const input = 'const greeting = "Hello, world!";\nexport default App;';
expect(redact(input)).toBe(input);
});
it('handles empty string input', () => {
expect(redact('')).toBe('');
});
});