-
Notifications
You must be signed in to change notification settings - Fork 97
Add pino logger with redaction for Stellar secret keys (issue #442) #482
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
Changes from all commits
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,19 @@ | ||
| import { redactObject } from "./logger"; | ||
|
|
||
| describe("logger redaction", () => { | ||
| test("redacts stellar secret keys in objects and strings", () => { | ||
| const secret = "S" + "A".repeat(55); | ||
| const obj = { | ||
| nested: { secretKey: secret, other: "ok" }, | ||
| token: secret, | ||
| arr: [secret, { privateKey: secret }], | ||
| } as const; | ||
|
|
||
| const redacted = redactObject(obj as any); | ||
|
|
||
| expect(redacted.nested.secretKey).toBe("[REDACTED]"); | ||
| expect(redacted.token).toBe("[REDACTED]"); | ||
| expect(redacted.arr[0]).toBe("[REDACTED]"); | ||
| expect(redacted.arr[1].privateKey).toBe("[REDACTED]"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import pino from "pino"; | ||
|
|
||
| const STELLAR_SECRET_REGEX = /^S[0-9A-Z]{55}$/; | ||
|
|
||
| function redactValue(value: unknown): unknown { | ||
| if (typeof value === "string" && STELLAR_SECRET_REGEX.test(value)) { | ||
| return "[REDACTED]"; | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| function redactObject(obj: any): any { | ||
| if (obj == null) return obj; | ||
| if (Array.isArray(obj)) return obj.map(redactObject); | ||
| if (typeof obj === "object") { | ||
| const out: Record<string, any> = {}; | ||
| for (const [k, v] of Object.entries(obj)) { | ||
| if (k === "secretKey" || k === "privateKey" || k === "seed") { | ||
| out[k] = "[REDACTED]"; | ||
| } else { | ||
| out[k] = redactObject(v); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| return redactValue(obj); | ||
| } | ||
|
|
||
| const logger = pino({ | ||
| level: process.env.LOG_LEVEL || "info", | ||
| // keep path-based redaction for structured fields | ||
| redact: { paths: ["*.secretKey", "*.privateKey", "*.seed"], censor: "[REDACTED]" }, | ||
| // ensure values (strings) that match Stellar secret pattern are redacted anywhere | ||
| formatters: { | ||
| log(obj: Record<string, any>) { | ||
| return redactObject(obj); | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| export { logger, redactObject, STELLAR_SECRET_REGEX }; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||
| import { Request, Response, NextFunction } from "express"; | ||||||
| import crypto from "crypto"; | ||||||
| import { logger } from "../logger"; | ||||||
|
|
||||||
| declare global { | ||||||
| namespace Express { | ||||||
|
|
@@ -30,16 +31,8 @@ export function requestLogger(req: Request, res: Response, next: NextFunction) { | |||||
| duration: `${duration}ms`, | ||||||
| }; | ||||||
|
|
||||||
| // ✅ STEP 4: Make logs readable in development | ||||||
| if (process.env.NODE_ENV === "production") { | ||||||
| // Machine-readable (for logging systems) | ||||||
| console.log(JSON.stringify(logEntry)); | ||||||
| } else { | ||||||
| // Human-readable (for local dev) | ||||||
| console.log( | ||||||
| `${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms | id=${requestId}` | ||||||
| ); | ||||||
| } | ||||||
| // ✅ STEP 4: Use structured logger with redaction | ||||||
| logger.info(logEntry, `${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms | id=${requestId}`); | ||||||
|
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. Avoid embedding raw URL data in the log message string. Line 35 duplicates Safer logging pattern- logger.info(logEntry, `${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms | id=${requestId}`);
+ logger.info(logEntry, "request_completed");📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }); | ||||||
|
|
||||||
| // ✅ STEP 5: Continue request | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an integration-style assertion against actual logger output.
Current coverage only tests
redactObject; it does not verify emitted log lines are redacted, which is part of the acceptance criteria. Also add an explicitseedfield assertion to cover all required paths.Suggested test expansion
import { redactObject } from "./logger"; +import { logger } from "./logger"; describe("logger redaction", () => { test("redacts stellar secret keys in objects and strings", () => { @@ - const obj = { + const obj = { nested: { secretKey: secret, other: "ok" }, + wallet: { seed: secret }, token: secret, arr: [secret, { privateKey: secret }], } as const; @@ expect(redacted.nested.secretKey).toBe("[REDACTED]"); + expect(redacted.wallet.seed).toBe("[REDACTED]"); expect(redacted.token).toBe("[REDACTED]"); expect(redacted.arr[0]).toBe("[REDACTED]"); expect(redacted.arr[1].privateKey).toBe("[REDACTED]"); }); + + test("does not emit raw stellar secrets in logger output", () => { + const secret = "S" + "A".repeat(55); + const spy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + + logger.info({ secretKey: secret, token: secret }, "test log"); + + const written = spy.mock.calls.map(([chunk]) => String(chunk)).join(""); + expect(written).not.toContain(secret); + expect(written).toContain("[REDACTED]"); + spy.mockRestore(); + }); });🤖 Prompt for AI Agents