Export a new countSensitiveMessages() function that returns how many messages in an array contain at least one sensitive pattern match. Checks both content and toolInput fields, counting each message at most once regardless of how many matches it contains. Tests verify zero counts for clean messages, correct counting with mixed sensitive/clean messages, and the single-count-per-message invariant when multiple secrets appear in one message. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
642 lines
23 KiB
TypeScript
642 lines
23 KiB
TypeScript
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 <noreply@anthropic.com>";
|
|
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);
|
|
});
|
|
});
|
|
});
|