From 0bdcca2f350978843d5553fb26f0a753e908129c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Feb 2026 03:34:31 +0000 Subject: [PATCH] test(whatsapp): add log redaction coverage --- CHANGELOG.md | 1 + src/web/auto-reply/heartbeat-runner.test.ts | 36 ++++++++++++++++++--- src/web/outbound.test.ts | 30 +++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a7cc756076..079ad76cb82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- WhatsApp/Logging: redact outbound recipient identifiers in WhatsApp outbound + heartbeat logs and remove message/poll preview text from those log lines. (#24980) Thanks @coygeek. - Telegram/Media SSRF: keep RFC2544 benchmark range (`198.18.0.0/15`) blocked by default, add an explicit SSRF-policy opt-in for Telegram media downloads, and keep other channels/URL fetch paths blocked. (#24982) Thanks @stakeswky. - Security/Shell env fallback: remove trusted-prefix shell-path fallback and only trust login shells explicitly registered in `/etc/shells`, defaulting to `/bin/sh` when `SHELL` is not registered. This ships in the next npm release. Thanks @tdjackey for reporting. - Security/Exec approvals: bind `host=node` approvals to explicit `nodeId`, reject cross-node replay of approved `system.run` requests, and include the target node in approval prompts. This ships in the next npm release. Thanks @tdjackey for reporting. diff --git a/src/web/auto-reply/heartbeat-runner.test.ts b/src/web/auto-reply/heartbeat-runner.test.ts index 78014787ad3..87d8d8a7ca9 100644 --- a/src/web/auto-reply/heartbeat-runner.test.ts +++ b/src/web/auto-reply/heartbeat-runner.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { getReplyFromConfig } from "../../auto-reply/reply.js"; import { HEARTBEAT_TOKEN } from "../../auto-reply/tokens.js"; +import { redactIdentifier } from "../../logging/redact-identifier.js"; import type { sendMessageWhatsApp } from "../outbound.js"; const state = vi.hoisted(() => ({ @@ -15,6 +16,10 @@ const state = vi.hoisted(() => ({ idleExpiresAt: null as number | null, }, events: [] as unknown[], + loggerInfoCalls: [] as unknown[][], + loggerWarnCalls: [] as unknown[][], + heartbeatInfoLogs: [] as string[], + heartbeatWarnLogs: [] as string[], })); vi.mock("../../agents/current-time.js", () => ({ @@ -64,15 +69,15 @@ vi.mock("../../infra/heartbeat-events.js", () => ({ vi.mock("../../logging.js", () => ({ getChildLogger: () => ({ - info: () => {}, - warn: () => {}, + info: (...args: unknown[]) => state.loggerInfoCalls.push(args), + warn: (...args: unknown[]) => state.loggerWarnCalls.push(args), }), })); vi.mock("./loggers.js", () => ({ whatsappHeartbeatLog: { - info: () => {}, - warn: () => {}, + info: (msg: string) => state.heartbeatInfoLogs.push(msg), + warn: (msg: string) => state.heartbeatWarnLogs.push(msg), }, })); @@ -115,6 +120,10 @@ describe("runWebHeartbeatOnce", () => { idleExpiresAt: null, }; state.events = []; + state.loggerInfoCalls = []; + state.loggerWarnCalls = []; + state.heartbeatInfoLogs = []; + state.heartbeatWarnLogs = []; senderMock = vi.fn(async () => ({ messageId: "m1" })); sender = senderMock as unknown as typeof sendMessageWhatsApp; @@ -187,4 +196,23 @@ describe("runWebHeartbeatOnce", () => { ]), ); }); + + it("redacts recipient and omits body preview in heartbeat logs", async () => { + replyResolverMock.mockResolvedValue({ text: "sensitive heartbeat body" }); + const { runWebHeartbeatOnce } = await getModules(); + await runWebHeartbeatOnce(buildRunArgs({ dryRun: true })); + + const expected = redactIdentifier("+123"); + const heartbeatLogs = state.heartbeatInfoLogs.join("\n"); + const childLoggerLogs = state.loggerInfoCalls.map((entry) => JSON.stringify(entry)).join("\n"); + + expect(heartbeatLogs).toContain(expected); + expect(heartbeatLogs).not.toContain("+123"); + expect(heartbeatLogs).not.toContain("sensitive heartbeat body"); + + expect(childLoggerLogs).toContain(expected); + expect(childLoggerLogs).not.toContain("+123"); + expect(childLoggerLogs).not.toContain("sensitive heartbeat body"); + expect(childLoggerLogs).not.toContain('"preview"'); + }); }); diff --git a/src/web/outbound.test.ts b/src/web/outbound.test.ts index 5f627b454ac..e60d15158fc 100644 --- a/src/web/outbound.test.ts +++ b/src/web/outbound.test.ts @@ -1,5 +1,10 @@ +import crypto from "node:crypto"; +import fsSync from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resetLogger, setLoggerOverride } from "../logging.js"; +import { redactIdentifier } from "../logging/redact-identifier.js"; import { setActiveWebListener } from "./active-listener.js"; const loadWebMediaMock = vi.fn(); @@ -154,6 +159,31 @@ describe("web outbound", () => { }); }); + it("redacts recipients and poll text in outbound logs", async () => { + const logPath = path.join(os.tmpdir(), `openclaw-outbound-${crypto.randomUUID()}.log`); + setLoggerOverride({ level: "trace", file: logPath }); + + await sendPollWhatsApp( + "+1555", + { question: "Lunch?", options: ["Pizza", "Sushi"], maxSelections: 1 }, + { verbose: false }, + ); + + await vi.waitFor( + () => { + expect(fsSync.existsSync(logPath)).toBe(true); + }, + { timeout: 2_000, interval: 5 }, + ); + + const content = fsSync.readFileSync(logPath, "utf-8"); + expect(content).toContain(redactIdentifier("+1555")); + expect(content).toContain(redactIdentifier("1555@s.whatsapp.net")); + expect(content).not.toContain(`"to":"+1555"`); + expect(content).not.toContain(`"jid":"1555@s.whatsapp.net"`); + expect(content).not.toContain("Lunch?"); + }); + it("sends reactions via active listener", async () => { await sendReactionWhatsApp("1555@s.whatsapp.net", "msg123", "✅", { verbose: false,