import { describe, it, expect } from "vitest"; import { redactSensitiveContent, redactMessage, redactString, countSensitiveMessages, } from "../../src/shared/sensitive-redactor.js"; import type { ParsedMessage } from "../../src/shared/types.js"; describe("sensitive-redactor", () => { describe("redactSensitiveContent", () => { // --- Tier 1: Known Secret Formats --- describe("AWS credentials", () => { it("redacts AWS access key IDs", () => { const input = "Use key AKIAIOSFODNN7EXAMPLE to authenticate"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[AWS_KEY]"); expect(result.sanitized).not.toContain("AKIAIOSFODNN7EXAMPLE"); expect(result.redactionCount).toBeGreaterThan(0); }); it("redacts ASIA temporary credentials", () => { const input = "Temporary creds: ASIA2EXAMPLEXEG4567Q"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[AWS_KEY]"); }); it("redacts AWS Bedrock API keys", () => { const key = "ABSK" + "A".repeat(110); const input = "key: " + key; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[AWS_BEDROCK_KEY]"); }); }); describe("GitHub tokens", () => { it("redacts GitHub PATs", () => { const token = "ghp_" + "a".repeat(36); const input = "token: " + token; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[GITHUB_TOKEN]"); expect(result.sanitized).not.toContain("ghp_"); }); it("redacts GitHub fine-grained PATs", () => { const token = "github_pat_" + "a".repeat(82); const result = redactSensitiveContent(token); expect(result.sanitized).toContain("[GITHUB_TOKEN]"); }); it("redacts GitHub app tokens (ghu_ and ghs_)", () => { const input = "ghu_" + "a".repeat(36) + " and ghs_" + "b".repeat(36); const result = redactSensitiveContent(input); expect(result.sanitized).not.toContain("ghu_"); expect(result.sanitized).not.toContain("ghs_"); expect(result.redactionCount).toBe(2); }); }); describe("GitLab tokens", () => { it("redacts GitLab PATs", () => { const input = "glpat-a1b2c3d4e5f6g7h8i9j0"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[GITLAB_TOKEN]"); }); it("redacts GitLab runner tokens", () => { const input = "glrt-a1b2c3d4e5f6g7h8i9j0"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[GITLAB_TOKEN]"); }); }); describe("OpenAI keys", () => { it("redacts OpenAI project keys", () => { const input = "sk-proj-" + "a".repeat(58) + "T3BlbkFJ" + "b".repeat(58); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[OPENAI_KEY]"); expect(result.sanitized).not.toContain("sk-proj-"); }); it("redacts legacy OpenAI keys", () => { const input = "sk-" + "a".repeat(20) + "T3BlbkFJ" + "b".repeat(20); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[OPENAI_KEY]"); }); }); describe("Anthropic keys", () => { it("redacts Anthropic API keys", () => { const input = "sk-ant-api03-" + "a".repeat(93) + "AA"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[ANTHROPIC_KEY]"); }); it("redacts Anthropic admin keys", () => { const input = "sk-ant-admin01-" + "a".repeat(93) + "AA"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[ANTHROPIC_KEY]"); }); }); describe("AI service tokens", () => { it("redacts HuggingFace tokens", () => { const input = "hf_" + "a".repeat(34); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[HF_TOKEN]"); }); it("redacts Perplexity keys", () => { const input = "pplx-" + "a".repeat(48); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[PERPLEXITY_KEY]"); }); }); describe("Stripe keys", () => { it("redacts live Stripe secret keys", () => { const input = "sk_live_abcdefghijklmnopqrstuvwxyz1234"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[STRIPE_KEY]"); expect(result.sanitized).not.toContain("sk_live_"); }); it("redacts test Stripe keys", () => { const input = "sk_test_abcdefghijklmnop"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[STRIPE_KEY]"); }); it("redacts restricted Stripe keys", () => { const input = "rk_live_abcdefghijklmnop"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[STRIPE_KEY]"); }); }); describe("Slack tokens", () => { it("redacts Slack bot tokens", () => { const input = "xoxb-1234567890123-1234567890123-abcdefghij"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[SLACK_TOKEN]"); }); it("redacts Slack webhook URLs", () => { const input = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[SLACK_WEBHOOK]"); }); }); describe("SendGrid tokens", () => { it("redacts SendGrid API tokens", () => { const sgKey = "SG." + "a".repeat(22) + "." + "b".repeat(43); const result = redactSensitiveContent(sgKey); expect(result.sanitized).toContain("[SENDGRID_TOKEN]"); }); }); describe("GCP API keys", () => { it("redacts GCP API keys", () => { const input = "AIza" + "a".repeat(35); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[GCP_KEY]"); }); }); describe("Heroku keys", () => { it("redacts Heroku API keys", () => { const input = "HRKU-AA" + "a".repeat(58); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[HEROKU_KEY]"); }); }); describe("npm tokens", () => { it("redacts npm access tokens", () => { const input = "npm_" + "a".repeat(36); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[NPM_TOKEN]"); }); }); describe("PyPI tokens", () => { it("redacts PyPI upload tokens", () => { const input = "pypi-AgEIcHlwaS5vcmc" + "a".repeat(50); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[PYPI_TOKEN]"); }); }); describe("Sentry tokens", () => { it("redacts Sentry user tokens", () => { const input = "sntryu_" + "a".repeat(64); const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[SENTRY_TOKEN]"); }); }); describe("JWT tokens", () => { it("redacts JWT tokens", () => { const header = Buffer.from('{"alg":"HS256"}').toString("base64url"); const payload = Buffer.from('{"sub":"1234567890"}').toString( "base64url" ); const sig = "abcdefghij"; const jwt = header + "." + payload + "." + sig; const result = redactSensitiveContent("Bearer " + jwt); expect(result.sanitized).toContain("[JWT]"); }); }); describe("Private keys (PEM)", () => { it("redacts RSA private keys", () => { const input = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn" + "a".repeat(100) + "\n-----END RSA PRIVATE KEY-----"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[PRIVATE_KEY]"); expect(result.sanitized).not.toContain("MIIEpAIBAAKCAQEA"); }); it("redacts generic private keys", () => { const input = "-----BEGIN PRIVATE KEY-----\n" + "a".repeat(100) + "\n-----END PRIVATE KEY-----"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[PRIVATE_KEY]"); }); }); describe("Generic API key pattern", () => { it("redacts KEY=value assignments with secret-like variable names", () => { const input = 'SECRET_KEY="verySecretValue1234567"'; const result = redactSensitiveContent(input); expect(result.redactionCount).toBeGreaterThan(0); }); it("redacts api_key assignments", () => { const input = "api_key: abcdefghijklmnopqrst"; const result = redactSensitiveContent(input); expect(result.redactionCount).toBeGreaterThan(0); }); it("redacts password assignments", () => { const input = 'password = "mySuperSecretPassword123"'; const result = redactSensitiveContent(input); expect(result.redactionCount).toBeGreaterThan(0); }); it("redacts mixed-case key assignments (case-insensitive keyword matching)", () => { const input = 'ApiKey = "abcdefghijklmnopqrst"'; const result = redactSensitiveContent(input); expect(result.redactionCount).toBeGreaterThan(0); }); it("redacts UPPER_CASE key assignments via generic pattern", () => { const input = 'AUTH_TOKEN: SuperSecretVal1234'; const result = redactSensitiveContent(input); expect(result.redactionCount).toBeGreaterThan(0); }); }); // --- Tier 2: PII/System Info --- describe("Home directory paths", () => { it("redacts /home/username paths", () => { const input = "Reading file /home/taylor/.ssh/id_rsa"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[HOME_PATH]"); expect(result.sanitized).not.toContain("/home/taylor"); }); it("redacts /Users/username paths (macOS)", () => { const input = "Found at /Users/taylor/projects/app/src/index.ts"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[HOME_PATH]"); expect(result.sanitized).not.toContain("/Users/taylor"); }); it("redacts C:\\Users\\username paths (Windows)", () => { const input = "Path: C:\\Users\\Taylor\\Documents\\project"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[HOME_PATH]"); }); }); describe("Connection strings", () => { it("redacts PostgreSQL connection strings", () => { const input = "DATABASE_URL=postgresql://admin:secret@db.example.com:5432/mydb"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[CONNECTION_STRING]"); expect(result.sanitized).not.toContain("admin:secret"); }); it("redacts MongoDB connection strings", () => { const input = "mongodb+srv://user:pass@cluster.mongodb.net/db"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[CONNECTION_STRING]"); }); it("redacts Redis connection strings", () => { const input = "redis://default:password@redis.example.com:6379"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[CONNECTION_STRING]"); }); }); describe("URLs with credentials", () => { it("redacts URLs with user:pass embedded", () => { const input = "https://admin:s3cret@api.example.com/v1/data"; const result = redactSensitiveContent(input); // Could match URL_WITH_CREDS or CONNECTION_STRING expect(result.redactionCount).toBeGreaterThan(0); expect(result.sanitized).not.toContain("admin:s3cret"); }); it("does NOT redact normal URLs without credentials", () => { const input = "https://api.example.com/v1/data"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); }); describe("Email addresses", () => { it("redacts email addresses", () => { const input = "Contact taylor@company.com for support"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[EMAIL]"); expect(result.sanitized).not.toContain("taylor@company.com"); }); it("does NOT redact example.com emails", () => { const input = "user@example.com is a placeholder"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); it("does NOT redact test.com emails", () => { const input = "test@test.com for testing"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); it("does NOT redact noreply@anthropic.com", () => { const input = "Co-Authored-By: Claude "; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); }); describe("IPv4 addresses", () => { it("redacts non-localhost IPv4 addresses", () => { const input = "Server running at 10.0.1.55:8080"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[IP_ADDR]"); expect(result.sanitized).not.toContain("10.0.1.55"); }); it("does NOT redact 127.0.0.1", () => { const input = "Listening on 127.0.0.1:3000"; const result = redactSensitiveContent(input); expect(result.sanitized).not.toContain("[IP_ADDR]"); expect(result.sanitized).toContain("127.0.0.1"); }); it("does NOT redact 0.0.0.0", () => { const input = "Bind to 0.0.0.0:8080"; const result = redactSensitiveContent(input); expect(result.sanitized).not.toContain("[IP_ADDR]"); }); it("does NOT redact RFC 5737 documentation IPs", () => { const input = "Example: 192.0.2.1 and 198.51.100.5 and 203.0.113.10"; const result = redactSensitiveContent(input); expect(result.sanitized).not.toContain("[IP_ADDR]"); }); }); describe("Bearer tokens", () => { it("redacts Bearer tokens in auth headers", () => { const input = "Authorization: Bearer some_long_token_value_here.with.parts"; const result = redactSensitiveContent(input); expect( result.sanitized.includes("[BEARER_TOKEN]") || result.sanitized.includes("[JWT]") ).toBe(true); }); }); describe("Environment variable secrets", () => { it("redacts SECRET_KEY assignments", () => { const input = "SECRET_KEY=abcdefghij1234567890"; const result = redactSensitiveContent(input); // May be matched by generic_api_key or env_var_secret depending on order expect(result.redactionCount).toBeGreaterThan(0); expect(result.sanitized).not.toContain("abcdefghij1234567890"); }); it("redacts DATABASE_PASSWORD assignments", () => { const input = 'DATABASE_PASSWORD="myDbPassw0rd"'; const result = redactSensitiveContent(input); expect(result.redactionCount).toBeGreaterThan(0); }); }); // --- False Positive Resistance --- describe("false positive resistance", () => { it("does NOT redact normal code content", () => { const input = "function add(a: number, b: number): number { return a + b; }"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); expect(result.redactionCount).toBe(0); }); it("does NOT redact normal markdown text", () => { const input = "# Getting Started\n\nTo install, run `npm install express`.\n\n## Features\n- Fast routing"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); it("does NOT redact short strings that could look like keys", () => { const input = "The sk value is 42"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); it("does NOT redact normal file paths outside home dirs", () => { const input = "Edit /etc/nginx/nginx.conf or /var/log/syslog"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); it("does NOT redact version numbers that look like IPs", () => { const input = "Using version 1.2.3 of the library"; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); }); it("preserves surrounding text when redacting", () => { const token = "ghp_" + "a".repeat(36); const input = "Before " + token + " after"; const result = redactSensitiveContent(input); expect(result.sanitized).toMatch( /^Before \[GITHUB_TOKEN\] after$/ ); }); }); // --- Edge Cases --- describe("edge cases", () => { it("handles empty string", () => { const result = redactSensitiveContent(""); expect(result.sanitized).toBe(""); expect(result.redactionCount).toBe(0); expect(result.categories).toHaveLength(0); }); it("handles string with no sensitive content", () => { const input = "This is a perfectly normal message with no secrets."; const result = redactSensitiveContent(input); expect(result.sanitized).toBe(input); expect(result.redactionCount).toBe(0); }); it("handles multiple different secrets in one string", () => { const ghToken = "ghp_" + "a".repeat(36); const input = "Key: " + ghToken + " and also AKIAIOSFODNN7EXAMPLE"; const result = redactSensitiveContent(input); expect(result.sanitized).toContain("[GITHUB_TOKEN]"); expect(result.sanitized).toContain("[AWS_KEY]"); expect(result.redactionCount).toBe(2); }); it("tracks matched categories", () => { const input = "ghp_" + "a".repeat(36); const result = redactSensitiveContent(input); expect(result.categories.length).toBeGreaterThan(0); }); }); }); describe("redactString", () => { it("returns just the sanitized string", () => { const input = "ghp_" + "a".repeat(36); const result = redactString(input); expect(result).toContain("[GITHUB_TOKEN]"); expect(typeof result).toBe("string"); }); }); describe("redactMessage", () => { const baseMessage: ParsedMessage = { uuid: "msg-1", category: "assistant_text", content: "Check ghp_" + "a".repeat(36) + " for access", toolName: undefined, toolInput: undefined, timestamp: "2025-10-15T10:00:00Z", rawIndex: 0, }; it("redacts the content field", () => { const redacted = redactMessage(baseMessage); expect(redacted.content).toContain("[GITHUB_TOKEN]"); expect(redacted.content).not.toContain("ghp_"); }); it("returns a new object without mutating the original", () => { const originalContent = baseMessage.content; const redacted = redactMessage(baseMessage); expect(redacted).not.toBe(baseMessage); expect(baseMessage.content).toBe(originalContent); }); it("preserves uuid, category, timestamp, rawIndex", () => { const redacted = redactMessage(baseMessage); expect(redacted.uuid).toBe(baseMessage.uuid); expect(redacted.category).toBe(baseMessage.category); expect(redacted.timestamp).toBe(baseMessage.timestamp); expect(redacted.rawIndex).toBe(baseMessage.rawIndex); }); it("redacts toolInput field", () => { const msg: ParsedMessage = { ...baseMessage, category: "tool_call", toolName: "Bash", toolInput: JSON.stringify({ command: "export SECRET_KEY=abcdefghijklmnopqrstuvwxyz", }), }; const redacted = redactMessage(msg); expect(redacted.toolInput).not.toBe(msg.toolInput); }); it("leaves toolName unchanged for standard tool names", () => { const msg: ParsedMessage = { ...baseMessage, category: "tool_call", toolName: "Read", toolInput: '{"file_path": "/etc/config"}', }; const redacted = redactMessage(msg); expect(redacted.toolName).toBe("Read"); }); it("handles undefined toolInput and toolName", () => { const redacted = redactMessage(baseMessage); expect(redacted.toolInput).toBeUndefined(); expect(redacted.toolName).toBeUndefined(); }); }); describe("countSensitiveMessages", () => { it("returns 0 for empty array", () => { const result = countSensitiveMessages([]); expect(result).toBe(0); }); it("returns 0 when no messages contain sensitive content", () => { const messages: ParsedMessage[] = [ { uuid: "m1", category: "assistant_text", content: "Hello, how can I help?", toolName: undefined, toolInput: undefined, timestamp: "2025-10-15T10:00:00Z", rawIndex: 0, }, ]; expect(countSensitiveMessages(messages)).toBe(0); }); it("counts messages with sensitive content", () => { const ghToken = "ghp_" + "a".repeat(36); const messages: ParsedMessage[] = [ { uuid: "m1", category: "assistant_text", content: "Here is your token: " + ghToken, toolName: undefined, toolInput: undefined, timestamp: "2025-10-15T10:00:00Z", rawIndex: 0, }, { uuid: "m2", category: "assistant_text", content: "No secrets here", toolName: undefined, toolInput: undefined, timestamp: "2025-10-15T10:01:00Z", rawIndex: 1, }, { uuid: "m3", category: "tool_call", content: "Running command", toolName: "Bash", toolInput: "export SECRET_KEY=abcdefghijklmnopqrstuvwxyz", timestamp: "2025-10-15T10:02:00Z", rawIndex: 2, }, ]; // m1 has a GitHub token in content, m3 has a secret in toolInput expect(countSensitiveMessages(messages)).toBe(2); }); it("counts a message only once even if it has multiple sensitive items", () => { const ghToken = "ghp_" + "a".repeat(36); const messages: ParsedMessage[] = [ { uuid: "m1", category: "assistant_text", content: ghToken + " and also AKIAIOSFODNN7EXAMPLE", toolName: undefined, toolInput: undefined, timestamp: "2025-10-15T10:00:00Z", rawIndex: 0, }, ]; // One message with two secrets still counts as 1 message expect(countSensitiveMessages(messages)).toBe(1); }); }); });