feat: add secret redaction utility
This commit is contained in:
+3
-2
@@ -11,14 +11,15 @@ Phase 0 complete. Phase 1 in progress.
|
||||
|
||||
## Current Phase
|
||||
|
||||
Phase 0 - Repository Setup
|
||||
Phase 1 - Core Utilities
|
||||
|
||||
## Completed Tasks
|
||||
|
||||
- Task 0.1 — Create repository skeleton ✅
|
||||
- Task 0.2 — package.json with dependencies ✅
|
||||
- Task 1.1 — Configuration loader ✅
|
||||
- Task 1.2 — Secret redaction utility (`src/utils/redact.js`) ✅
|
||||
|
||||
## Next Task
|
||||
|
||||
Task 1.2 — Secret redaction utility (`src/utils/redact.js`)
|
||||
Task 1.3 — Context budget utility (`src/utils/context-budget.js`)
|
||||
|
||||
@@ -60,7 +60,7 @@ Requirements:
|
||||
- Replace detected secrets with `[REDACTED]`.
|
||||
- Return the redacted string.
|
||||
|
||||
Status: ⏳ Pending
|
||||
Status: ✅ Complete
|
||||
|
||||
### Task 1.3 - Context budget utility
|
||||
|
||||
|
||||
@@ -1 +1,29 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user