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 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 13:35:15 -05:00
parent 4027dd65be
commit 6681f07fc0
2 changed files with 97 additions and 0 deletions

View File

@@ -449,3 +449,25 @@ export function redactMessage(msg: ParsedMessage): ParsedMessage {
// toolName is typically safe (e.g. "Bash", "Read") — pass through unchanged // 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;
}

View File

@@ -3,6 +3,7 @@ import {
redactSensitiveContent, redactSensitiveContent,
redactMessage, redactMessage,
redactString, redactString,
countSensitiveMessages,
} from "../../src/shared/sensitive-redactor.js"; } from "../../src/shared/sensitive-redactor.js";
import type { ParsedMessage } from "../../src/shared/types.js"; import type { ParsedMessage } from "../../src/shared/types.js";
@@ -563,4 +564,78 @@ describe("sensitive-redactor", () => {
expect(redacted.toolName).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);
});
});
}); });