-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(core,webapp): redact sensitive fields in logs by default and cap their size #4401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8c4f389
1c8dfbf
858f056
04baadc
fb88613
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@trigger.dev/core": patch | ||
| --- | ||
|
|
||
| Redact common credential and sensitive-data fields from structured logger output by default, including nested values and error metadata. Long strings and arrays are now truncated to keep log entries manageable. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import type { LogLevel } from "@trigger.dev/core/logger"; | ||
| import { Logger } from "@trigger.dev/core/logger"; | ||
| import { Logger, redact } from "@trigger.dev/core/logger"; | ||
| import { patchConsoleToTelnet, startTelnetLogServer } from "@trigger.dev/core/v3/telnetLogServer"; | ||
| import { sensitiveDataReplacer } from "./sensitiveDataReplacer"; | ||
| import { AsyncLocalStorage } from "async_hooks"; | ||
|
|
@@ -12,24 +12,40 @@ export function trace<T>(fields: Record<string, unknown>, fn: () => T): T { | |
| return currentFieldsStore.run(fields, fn); | ||
| } | ||
|
|
||
| // The keys below aren't already in the Logger's default deny-list. Passing them here means the | ||
| // extra data sent to Sentry gets the same redaction as the stdout line, instead of bypassing it. | ||
| const SENTRY_EXTRA_FILTERED_KEYS = ["examples", "connectionString"]; | ||
|
|
||
| Logger.onError = (message, ...args) => { | ||
| const error = extractErrorFromArgs(args); | ||
| const extra = redact(flattenArgs(args), SENTRY_EXTRA_FILTERED_KEYS) as Record<string, unknown>; | ||
|
|
||
| if (error) { | ||
| captureException(error, { | ||
| captureException(redactError(error), { | ||
| extra: { | ||
| message, | ||
| ...flattenArgs(args), | ||
| ...extra, | ||
| }, | ||
| }); | ||
| } else { | ||
|
Comment on lines
+21
to
30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Captured Sentry extra still carries the raw Error object On the Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| captureMessage(message, { | ||
| level: "error", | ||
| extra: flattenArgs(args), | ||
| extra, | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| function redactError(error: Error): Error { | ||
| const redactedError = new Error(redact(error.message) as string); | ||
| redactedError.name = error.name; | ||
|
|
||
| if (error.stack) { | ||
| redactedError.stack = redact(error.stack) as string; | ||
| } | ||
|
|
||
| return redactedError; | ||
| } | ||
|
|
||
| function extractErrorFromArgs(args: Array<Record<string, unknown> | undefined>) { | ||
| for (const arg of args) { | ||
| if (arg && "error" in arg && arg.error instanceof Error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| const captureExceptionMock = vi.fn(); | ||
| const captureMessageMock = vi.fn(); | ||
|
|
||
| vi.mock("@sentry/remix", () => ({ | ||
| captureException: captureExceptionMock, | ||
| captureMessage: captureMessageMock, | ||
| })); | ||
|
|
||
| describe("logger.server Logger.onError", () => { | ||
| beforeEach(() => { | ||
| captureExceptionMock.mockClear(); | ||
| captureMessageMock.mockClear(); | ||
| vi.spyOn(console, "error").mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| it("redacts the extra payload sent to Sentry on the captureMessage path", async () => { | ||
| const { logger } = await import("~/services/logger.server"); | ||
|
|
||
| logger.error("something failed", { | ||
| payload: { secret: "do-not-leak" }, | ||
| apiKey: "tr_prod_should_not_leak", | ||
| }); | ||
|
|
||
| expect(captureMessageMock).toHaveBeenCalledTimes(1); | ||
| const [, options] = captureMessageMock.mock.calls[0] as [string, { extra: unknown }]; | ||
|
|
||
| expect(JSON.stringify(options.extra)).not.toContain("do-not-leak"); | ||
| expect(JSON.stringify(options.extra)).not.toContain("tr_prod_should_not_leak"); | ||
| }); | ||
|
|
||
| it("redacts the exception and extra payload sent to Sentry", async () => { | ||
| const { logger } = await import("~/services/logger.server"); | ||
| const error = new Error("tr_prod_should_not_leak"); | ||
| error.stack = "Bearer secret-token"; | ||
|
|
||
| logger.error("boom", { | ||
| error, | ||
| payload: { secret: "do-not-leak" }, | ||
| }); | ||
|
|
||
| expect(captureExceptionMock).toHaveBeenCalledTimes(1); | ||
| const [capturedError, options] = captureExceptionMock.mock.calls[0] as [ | ||
| Error, | ||
| { extra: unknown }, | ||
| ]; | ||
|
|
||
| expect(capturedError).not.toBe(error); | ||
| expect(capturedError.message).not.toContain("tr_prod_should_not_leak"); | ||
| expect(capturedError.stack).not.toContain("secret-token"); | ||
| expect(JSON.stringify(options.extra)).not.toContain("do-not-leak"); | ||
| }); | ||
|
|
||
| it("still forwards non-sensitive extra fields", async () => { | ||
| const { logger } = await import("~/services/logger.server"); | ||
|
|
||
| logger.error("something failed", { runId: "run_123", keep: "this stays" }); | ||
|
|
||
| expect(captureMessageMock).toHaveBeenCalledTimes(1); | ||
| const [, options] = captureMessageMock.mock.calls[0] as [ | ||
| string, | ||
| { extra: Record<string, unknown> }, | ||
| ]; | ||
|
|
||
| expect(options.extra.runId).toBe("run_123"); | ||
| expect(options.extra.keep).toBe("this stays"); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.