Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions assistant/src/util/log-redact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Pino log serializers that scrub sensitive data (bearer tokens, API keys,
* authorization headers) from logged values. Applied to every pino instance
* so secrets never reach log files even when errors bubble up opaque objects.
*
* API-key patterns are intentionally duplicated from security/secret-scanner.ts
* rather than imported — the scanner carries entropy analysis, encoded-secret
* detection, and other heavyweight logic that a hot-path serializer should not
* pull in.
*/

// ---------------------------------------------------------------------------
// Sensitive-value patterns (subset of secret-scanner PATTERNS)
// ---------------------------------------------------------------------------

const BEARER_RE = /Bearer [A-Za-z0-9._\-]+/g;

const API_KEY_PATTERNS: RegExp[] = [
// AWS
/AKIA[0-9A-Z]{16}/g,
// GitHub
/gh[pousr]_[A-Za-z0-9_]{36,255}/g,
/github_pat_[A-Za-z0-9_]{22,255}/g,
// GitLab
/glpat-[A-Za-z0-9\-_]{20,}/g,
// Stripe
/sk_live_[A-Za-z0-9]{24,}/g,
/rk_live_[A-Za-z0-9]{24,}/g,
// Slack
/xoxb-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}/g,
/xoxp-[0-9]{10,}-[0-9]{10,}-[0-9]{10,}-[a-f0-9]{32}/g,
// Anthropic
/sk-ant-[A-Za-z0-9\-_]{80,}/g,
// OpenAI
/sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}/g,
/sk-proj-[A-Za-z0-9\-_]{40,}/g,
// Google
/AIza[A-Za-z0-9\-_]{35}/g,
/GOCSPX-[A-Za-z0-9\-_]{28}/g,
// SendGrid
/SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}/g,
// Telegram bot token
/[0-9]{8,10}:[A-Za-z0-9_-]{35}/g,
// npm
/npm_[A-Za-z0-9]{36}/g,
];

// Header names whose values should always be fully redacted
const SENSITIVE_HEADERS = new Set([
'authorization',
'proxy-authorization',
'cookie',
'set-cookie',
'x-api-key',
'x-auth-token',
]);

// ---------------------------------------------------------------------------
// String redaction
// ---------------------------------------------------------------------------

function redactString(value: string): string {
let result = value;

// Redact bearer tokens
result = result.replace(BEARER_RE, 'Bearer [REDACTED]');

// Redact API key patterns
for (const pattern of API_KEY_PATTERNS) {
pattern.lastIndex = 0;
result = result.replace(pattern, '[REDACTED]');
}

return result;
}

// ---------------------------------------------------------------------------
// Deep value redaction — walks objects/arrays and scrubs strings in place
// ---------------------------------------------------------------------------

function redactValue(value: unknown, depth: number): unknown {
if (depth > 8) return value;

if (typeof value === 'string') {
return redactString(value);
}

if (Array.isArray(value)) {
return value.map((item) => redactValue(item, depth + 1));
}

if (value !== null && typeof value === 'object') {
const result: Record<string, unknown> = {};
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
Comment thread
siddseethepalli marked this conversation as resolved.
const lowerKey = key.toLowerCase();
// Fully redact sensitive header values
if (SENSITIVE_HEADERS.has(lowerKey)) {
result[key] = '[REDACTED]';
} else {
result[key] = redactValue(val, depth + 1);
}
}
return result;
}

return value;
}

// ---------------------------------------------------------------------------
// Pino serializers
// ---------------------------------------------------------------------------

/**
* Pino serializer for the `err` binding — redacts secrets from error messages,
* stacks, and any attached properties.
*/
function errSerializer(err: unknown): unknown {
return redactValue(err, 0);
}

/**
* Pino serializer for `req` (HTTP request objects) — redacts authorization
* headers and sensitive values in the URL/body.
*/
function reqSerializer(req: unknown): unknown {
return redactValue(req, 0);
}

/**
* Pino serializer for `res` (HTTP response objects) — redacts sensitive
* header values that may appear in response logs.
*/
function resSerializer(res: unknown): unknown {
return redactValue(res, 0);
}

/**
* Pino serializers config object. Spread this into the pino options `serializers`
* field on every logger instance.
*/
export const logSerializers: Record<string, (value: unknown) => unknown> = {
err: errSerializer,
req: reqSerializer,
res: resSerializer,
};

// Exported for testing
export { redactString as _redactString, redactValue as _redactValue };
19 changes: 10 additions & 9 deletions assistant/src/util/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from 'node:path';
import { Writable } from 'node:stream';
import pino from 'pino';
import pinoPretty from 'pino-pretty';
import { logSerializers } from './log-redact.js';
import { getLogPath } from './platform.js';

export type LogFileConfig = {
Expand Down Expand Up @@ -55,7 +56,7 @@ let activeLogFileConfig: LogFileConfig | null = null;

function buildRotatingLogger(config: LogFileConfig): pino.Logger {
if (!config.dir) {
return pino({ name: 'assistant' }, pinoPretty({ destination: 1 }));
return pino({ name: 'assistant', serializers: logSerializers }, pinoPretty({ destination: 1 }));
}

if (!existsSync(config.dir)) {
Expand All @@ -74,7 +75,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
if (process.env.VELLUM_DEBUG === '1') {
const prettyStream = pinoPretty({ destination: 2 });
return pino(
{ name: 'assistant', level },
{ name: 'assistant', level, serializers: logSerializers },
pino.multistream([
{ stream: fileStream, level: 'info' as const },
{ stream: prettyStream, level: 'debug' as const },
Expand All @@ -83,7 +84,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
}

return pino(
{ name: 'assistant', level },
{ name: 'assistant', level, serializers: logSerializers },
pino.multistream([
{ stream: fileStream, level: 'info' as const },
{ stream: pinoPretty({ destination: 1 }), level: 'info' as const },
Expand Down Expand Up @@ -121,7 +122,7 @@ function getRootLogger(): pino.Logger {
|| process.env.VELLUM_LOG_STDERR === '1';
if (forceStderr) {
rootLogger = pino(
{ level: process.env.VELLUM_DEBUG === '1' ? 'debug' : 'info' },
{ level: process.env.VELLUM_DEBUG === '1' ? 'debug' : 'info', serializers: logSerializers },
pino.destination(2),
);
return rootLogger;
Expand All @@ -136,20 +137,20 @@ function getRootLogger(): pino.Logger {
{ stream: fileStream, level: 'info' as const },
{ stream: prettyStream, level: 'debug' as const },
]);
rootLogger = pino({ level: 'debug' }, multi);
rootLogger = pino({ level: 'debug', serializers: logSerializers }, multi);
} else if (process.env.DEBUG_STDOUT_LOGS === '1') {
rootLogger = pino(
{ level: 'info' },
{ level: 'info', serializers: logSerializers },
pino.multistream([
{ stream: fileStream, level: 'info' as const },
{ stream: pinoPretty({ destination: 1 }), level: 'info' as const },
]),
);
} else {
rootLogger = pino({ level: 'info' }, fileStream);
rootLogger = pino({ level: 'info', serializers: logSerializers }, fileStream);
}
} catch {
rootLogger = pino({ level: process.env.VELLUM_DEBUG === '1' ? 'debug' : 'info' }, pinoPretty({ destination: 2 }));
rootLogger = pino({ level: process.env.VELLUM_DEBUG === '1' ? 'debug' : 'info', serializers: logSerializers }, pinoPretty({ destination: 2 }));
}
}
return rootLogger;
Expand Down Expand Up @@ -225,7 +226,7 @@ export function getCliLogger(name: string): pino.Logger {
get(_target, prop, receiver) {
if (!logger) {
logger = pino(
{ name, level: 'trace' },
{ name, level: 'trace', serializers: logSerializers },
pino.multistream([
{ stream: cliDestination(1, 49), level: 'trace' as const },
{ stream: cliDestination(2), level: 'error' as const },
Expand Down
97 changes: 97 additions & 0 deletions gateway/src/log-redact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Pino log serializers that scrub sensitive data (bearer tokens, API keys,
* authorization headers) from logged values.
*
* This is a standalone copy for the gateway package — kept in sync with
* assistant/src/util/log-redact.ts. The gateway has no dependency on the
* assistant package, so we duplicate the lightweight serializer rather than
* adding a cross-package import.
*/

// ---------------------------------------------------------------------------
// Sensitive-value patterns
// ---------------------------------------------------------------------------

const BEARER_RE = /Bearer [A-Za-z0-9._\-]+/g;

const API_KEY_PATTERNS: RegExp[] = [
/AKIA[0-9A-Z]{16}/g,
/gh[pousr]_[A-Za-z0-9_]{36,255}/g,
/github_pat_[A-Za-z0-9_]{22,255}/g,
/glpat-[A-Za-z0-9\-_]{20,}/g,
/sk_live_[A-Za-z0-9]{24,}/g,
/rk_live_[A-Za-z0-9]{24,}/g,
/xoxb-[0-9]{10,}-[0-9]{10,}-[A-Za-z0-9]{24,}/g,
/xoxp-[0-9]{10,}-[0-9]{10,}-[0-9]{10,}-[a-f0-9]{32}/g,
/sk-ant-[A-Za-z0-9\-_]{80,}/g,
/sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}/g,
/sk-proj-[A-Za-z0-9\-_]{40,}/g,
/AIza[A-Za-z0-9\-_]{35}/g,
/GOCSPX-[A-Za-z0-9\-_]{28}/g,
/SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}/g,
/[0-9]{8,10}:[A-Za-z0-9_-]{35}/g,
/npm_[A-Za-z0-9]{36}/g,
];

const SENSITIVE_HEADERS = new Set([
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"x-api-key",
"x-auth-token",
]);

// ---------------------------------------------------------------------------
// String redaction
// ---------------------------------------------------------------------------

function redactString(value: string): string {
let result = value;
result = result.replace(BEARER_RE, "Bearer [REDACTED]");
for (const pattern of API_KEY_PATTERNS) {
pattern.lastIndex = 0;
result = result.replace(pattern, "[REDACTED]");
}
return result;
}

// ---------------------------------------------------------------------------
// Deep value redaction
// ---------------------------------------------------------------------------

function redactValue(value: unknown, depth: number): unknown {
if (depth > 8) return value;

if (typeof value === "string") {
return redactString(value);
}

if (Array.isArray(value)) {
return value.map((item) => redactValue(item, depth + 1));
}

if (value !== null && typeof value === "object") {
const result: Record<string, unknown> = {};
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
result[key] = "[REDACTED]";
} else {
result[key] = redactValue(val, depth + 1);
}
}
return result;
}

return value;
}

// ---------------------------------------------------------------------------
// Pino serializers
// ---------------------------------------------------------------------------

export const logSerializers: Record<string, (value: unknown) => unknown> = {
err: (err) => redactValue(err, 0),
req: (req) => redactValue(req, 0),
res: (res) => redactValue(res, 0),
};
5 changes: 3 additions & 2 deletions gateway/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import pino from "pino";
import pinoPretty from "pino-pretty";
import { logSerializers } from "./log-redact.js";

export type LogFileConfig = {
dir: string | undefined;
Expand Down Expand Up @@ -53,7 +54,7 @@ let activeConfig: LogFileConfig | null = null;

function buildLogger(config: LogFileConfig | null): pino.Logger {
if (!config?.dir) {
return pino({ name: "gateway" }, pinoPretty({ destination: 1 }));
return pino({ name: "gateway", serializers: logSerializers }, pinoPretty({ destination: 1 }));
}

if (!existsSync(config.dir)) {
Expand All @@ -68,7 +69,7 @@ function buildLogger(config: LogFileConfig | null): pino.Logger {
activeConfig = config;

return pino(
{ name: "gateway" },
{ name: "gateway", serializers: logSerializers },
pino.multistream([
{ stream: fileStream, level: "info" as const },
{ stream: pinoPretty({ destination: 1 }), level: "info" as const },
Expand Down
Loading