From 6681f07fc059d38803267dfeb7e73782c0bb9c30 Mon Sep 17 00:00:00 2001 From: teernisse Date: Fri, 30 Jan 2026 13:35:15 -0500 Subject: [PATCH] Add countSensitiveMessages for pre-scan sensitive content detection 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 --- src/shared/sensitive-redactor.ts | 22 ++++++++ tests/unit/sensitive-redactor.test.ts | 75 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/src/shared/sensitive-redactor.ts b/src/shared/sensitive-redactor.ts index 8d2bd40..b1ea506 100644 --- a/src/shared/sensitive-redactor.ts +++ b/src/shared/sensitive-redactor.ts @@ -449,3 +449,25 @@ export function redactMessage(msg: ParsedMessage): ParsedMessage { // toolName is typically safe (e.g. "Bash", "Read") — pass through unchanged }; } + +/** + * Counts how many messages contain at least one sensitive match. + * Checks both content and toolInput fields. + */ +export function countSensitiveMessages(messages: ParsedMessage[]): number { + let count = 0; + for (const msg of messages) { + const contentResult = redactSensitiveContent(msg.content); + if (contentResult.redactionCount > 0) { + count++; + continue; + } + if (msg.toolInput) { + const inputResult = redactSensitiveContent(msg.toolInput); + if (inputResult.redactionCount > 0) { + count++; + } + } + } + return count; +} diff --git a/tests/unit/sensitive-redactor.test.ts b/tests/unit/sensitive-redactor.test.ts index 4bc4c1d..fd20739 100644 --- a/tests/unit/sensitive-redactor.test.ts +++ b/tests/unit/sensitive-redactor.test.ts @@ -3,6 +3,7 @@ import { redactSensitiveContent, redactMessage, redactString, + countSensitiveMessages, } from "../../src/shared/sensitive-redactor.js"; import type { ParsedMessage } from "../../src/shared/types.js"; @@ -563,4 +564,78 @@ describe("sensitive-redactor", () => { 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); + }); + }); });