mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:20:42 +00:00
fix(logging): redact console and file sinks
This commit is contained in:
@@ -71,6 +71,9 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Plugins/CLI: preserve manifest name, description, format, and source metadata in cold `openclaw plugins list` output without importing plugin runtime. Thanks @shakkernerd.
|
||||
- Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd.
|
||||
- Logging: redact configured secret patterns at console and file-log sink exits
|
||||
so credentials that reach the logger are masked before terminal display or
|
||||
JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan.
|
||||
- Agents/groups: treat clean empty assistant stops as silent `NO_REPLY` only for always-on groups where silent replies are allowed, while keeping direct and mention-gated sessions on the incomplete-turn retry path. Thanks @MagnaAI.
|
||||
- macOS/Node: keep native remote app nodes from advertising `browser.proxy`,
|
||||
start browser-capable CLI node services through the restored
|
||||
|
||||
@@ -167,7 +167,9 @@ Tool summaries can redact sensitive tokens before they hit the console:
|
||||
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
|
||||
- `logging.redactPatterns`: list of regex strings to override the default set
|
||||
|
||||
Redaction affects **console output only** and does not alter file logs.
|
||||
Redaction applies at the logging sinks for **console output**, **stderr-routed
|
||||
console diagnostics**, and **file logs**. File logs stay JSONL, but matching
|
||||
secret values are masked before the line is written to disk.
|
||||
|
||||
## Diagnostics and OpenTelemetry
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
describe("enableConsoleCapture", () => {
|
||||
const secret = "sk-testsecret1234567890abcd";
|
||||
|
||||
it("swallows EIO from stderr writes", () => {
|
||||
setLoggerOverride({ level: "info", file: tempLogPath() });
|
||||
vi.spyOn(process.stderr, "write").mockImplementation(() => {
|
||||
@@ -123,6 +125,50 @@ describe("enableConsoleCapture", () => {
|
||||
expect(stdoutWrite).toHaveBeenCalledWith('{\n "ok": true\n}\n');
|
||||
});
|
||||
|
||||
it("redacts credentials before forwarding console output", () => {
|
||||
setLoggerOverride({ level: "info", file: tempLogPath() });
|
||||
const log = vi.fn();
|
||||
console.log = log;
|
||||
enableConsoleCapture();
|
||||
|
||||
console.log("apiKey:", secret);
|
||||
|
||||
expect(log).toHaveBeenCalledTimes(1);
|
||||
const line = String(log.mock.calls[0]?.[0] ?? "");
|
||||
expect(line).toContain("apiKey:");
|
||||
expect(line).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("redacts credentials before writing forced stderr console output", () => {
|
||||
setLoggerOverride({ level: "info", file: tempLogPath() });
|
||||
const stderrWrite = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
routeLogsToStderr();
|
||||
enableConsoleCapture();
|
||||
|
||||
console.error(`Authorization: Bearer ${secret}`);
|
||||
|
||||
expect(stderrWrite).toHaveBeenCalledTimes(1);
|
||||
const line = String(stderrWrite.mock.calls[0]?.[0] ?? "");
|
||||
expect(line).toContain("Authorization: Bearer");
|
||||
expect(line).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("redacts credentials when timestamp prefixing console output", () => {
|
||||
setLoggerOverride({ level: "info", file: tempLogPath() });
|
||||
const warn = vi.fn();
|
||||
console.warn = warn;
|
||||
setConsoleTimestampPrefix(true);
|
||||
enableConsoleCapture();
|
||||
|
||||
console.warn(`token=${secret}`);
|
||||
|
||||
expect(warn).toHaveBeenCalledTimes(1);
|
||||
const line = String(warn.mock.calls[0]?.[0] ?? "");
|
||||
expect(line).toMatch(/^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T)/);
|
||||
expect(line).toContain("token=");
|
||||
expect(line).not.toContain(secret);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "stdout", stream: process.stdout },
|
||||
{ name: "stderr", stream: process.stderr },
|
||||
|
||||
@@ -7,6 +7,7 @@ import { resolveEnvLogLevelOverride } from "./env-log-level.js";
|
||||
import { type LogLevel, normalizeLogLevel } from "./levels.js";
|
||||
import { getLogger } from "./logger.js";
|
||||
import { resolveNodeRequireFromMeta } from "./node-require.js";
|
||||
import { redactSensitiveText } from "./redact.js";
|
||||
import { loggingState } from "./state.js";
|
||||
import { formatLocalIsoWithOffset, formatTimestamp } from "./timestamps.js";
|
||||
import type { ConsoleStyle, LoggerSettings } from "./types.js";
|
||||
@@ -275,7 +276,8 @@ export function enableConsoleCapture(): void {
|
||||
if (loggingState.forceConsoleToStderr) {
|
||||
// In --json mode, all console.* writes are diagnostics and should stay off stdout.
|
||||
try {
|
||||
const line = timestamp ? `${timestamp} ${formatted}` : formatted;
|
||||
const redacted = redactSensitiveText(formatted);
|
||||
const line = timestamp ? `${timestamp} ${redacted}` : redacted;
|
||||
process.stderr.write(`${line}\n`);
|
||||
} catch (err) {
|
||||
if (isEpipeError(err)) {
|
||||
@@ -285,19 +287,16 @@ export function enableConsoleCapture(): void {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const redacted = redactSensitiveText(formatted);
|
||||
if (!timestamp) {
|
||||
orig.apply(console, args as []);
|
||||
if (args.length === 0) {
|
||||
orig.apply(console, args as []);
|
||||
return;
|
||||
}
|
||||
orig.call(console, redacted);
|
||||
return;
|
||||
}
|
||||
if (args.length === 0) {
|
||||
orig.call(console, timestamp);
|
||||
return;
|
||||
}
|
||||
if (typeof args[0] === "string") {
|
||||
orig.call(console, `${timestamp} ${args[0]}`, ...args.slice(1));
|
||||
return;
|
||||
}
|
||||
orig.call(console, timestamp, ...args);
|
||||
orig.call(console, redacted ? `${timestamp} ${redacted}` : timestamp);
|
||||
} catch (err) {
|
||||
if (isEpipeError(err)) {
|
||||
return;
|
||||
|
||||
45
src/logging/logger-redaction-behavior.test.ts
Normal file
45
src/logging/logger-redaction-behavior.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import fs from "node:fs";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { getLogger, resetLogger, setLoggerOverride } from "../logging.js";
|
||||
import { createSuiteLogPathTracker } from "./log-test-helpers.js";
|
||||
|
||||
const secret = "sk-testsecret1234567890abcd";
|
||||
const logPathTracker = createSuiteLogPathTracker("openclaw-log-redaction-");
|
||||
|
||||
beforeAll(async () => {
|
||||
await logPathTracker.setup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetLogger();
|
||||
setLoggerOverride(null);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await logPathTracker.cleanup();
|
||||
});
|
||||
|
||||
describe("file log redaction", () => {
|
||||
it("redacts credential fields before writing JSONL file logs", () => {
|
||||
const logPath = logPathTracker.nextPath();
|
||||
setLoggerOverride({ level: "info", file: logPath });
|
||||
|
||||
getLogger().info({ apiKey: secret, message: "provider configured" });
|
||||
|
||||
const content = fs.readFileSync(logPath, "utf8");
|
||||
expect(content).toContain("provider configured");
|
||||
expect(content).toContain('"apiKey"');
|
||||
expect(content).not.toContain(secret);
|
||||
});
|
||||
|
||||
it("redacts bearer tokens in file log message strings", () => {
|
||||
const logPath = logPathTracker.nextPath();
|
||||
setLoggerOverride({ level: "info", file: logPath });
|
||||
|
||||
getLogger().warn({ message: `Authorization: Bearer ${secret}` });
|
||||
|
||||
const content = fs.readFileSync(logPath, "utf8");
|
||||
expect(content).toContain("Authorization: Bearer");
|
||||
expect(content).not.toContain(secret);
|
||||
});
|
||||
});
|
||||
@@ -423,7 +423,7 @@ function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
|
||||
logger.attachTransport((logObj: LogObj) => {
|
||||
try {
|
||||
const time = formatTimestamp(logObj.date ?? new Date(), { style: "long" });
|
||||
const line = JSON.stringify({ ...logObj, time });
|
||||
const line = redactSensitiveText(JSON.stringify({ ...logObj, time }));
|
||||
const payload = `${line}\n`;
|
||||
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
||||
const nextBytes = currentFileBytes + payloadBytes;
|
||||
|
||||
Reference in New Issue
Block a user