Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"src/lib/messaging/channels/index.ts": 25,
"src/lib/onboard/gateway-binding.ts": 52,
"src/lib/runner.ts": 87,
"src/lib/security/redact.ts": 53,
"src/lib/security/redact.ts": 54,
"src/lib/state/onboard-session.ts": 37,
"src/lib/state/registry.ts": 101,
"src/lib/state/state-root.ts": 21,
Expand Down
28 changes: 26 additions & 2 deletions src/lib/actions/sandbox/gateway-wedge-diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,24 @@ describe("collectGatewayWedgeDiagnostics wedge signature (#4710)", () => {
stdout:
"gateway startup failed: Authorization: Bearer abc.def.ghi rejected\n" +
'gateway startup failed: api_key="nv-secret-123" invalid\n' +
"gateway startup failed: \u001b[31mboom\u001b[0m Process will stay alive\n",
"gateway startup failed: \u001b[31mboom\u001b[0m Process will stay alive\n" +
"gateway startup failed: env OPENAI_API_KEY=example-not-a-real-value-0001\n" +
"gateway startup failed: SLACK_BOT_TOKEN=example-not-a-real-value-0002\n" +
"Process will stay alive; TELEGRAM_BOT_TOKEN=example-not-a-real-value-0003\n",
stderr: "",
}));
expect(lines[0]).toBe("gateway startup failed: Authorization: Bearer [REDACTED] rejected");
expect(lines[1]).toBe('gateway startup failed: api_key="[REDACTED] invalid');
expect(lines[1]).toBe('gateway startup failed: api_key="[REDACTED]" invalid');
// Terminal escape sequences are stripped so sandbox output cannot forge
// operator-terminal content.
expect(lines[2]).toBe("gateway startup failed: [31mboom[0m Process will stay alive");
expect(lines[2]).not.toContain("\u001b");
// Underscore-separated credential env names need the shared redactor: `\b`
// does not match between `_` and a word character, so the local patterns
// above never reach the assignment.
expect(lines[3]).toBe("gateway startup failed: env OPENAI_API_KEY=<REDACTED>");
expect(lines[4]).toBe("gateway startup failed: SLACK_BOT_TOKEN=<REDACTED>");
expect(lines[5]).toBe("Process will stay alive; TELEGRAM_BOT_TOKEN=<REDACTED>");
});
});

Expand All @@ -64,6 +73,21 @@ describe("sanitizeWedgeLogLine", () => {
);
});

it.each([
['OPENAI_API_KEY="opaque api key value"', "opaque api key value"],
["SERVICE_TOKEN='opaque token value'", "opaque token value"],
['DATABASE_PASSWORD="opaque password value"', "opaque password value"],
["WEBHOOK_SECRET='opaque secret value'", "opaque secret value"],
])("redacts the complete quoted value in %s", (assignment, secret) => {
const sanitized = sanitizeWedgeLogLine(
`gateway startup failed: ${assignment}; safe diagnostic remains`,
);

expect(sanitized).not.toContain(secret);
expect(sanitized).toContain("REDACTED");
expect(sanitized).toContain("; safe diagnostic remains");
});

it("leaves ordinary wedge lines untouched", () => {
const line =
"gateway startup failed: listen EADDRINUSE. Process will stay alive; fix the issue and restart.";
Expand Down
25 changes: 17 additions & 8 deletions src/lib/actions/sandbox/gateway-wedge-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// defaulted off.

import { shellQuote } from "../../runner";
import { redactFull } from "../../security/redact";
import type { SandboxCommandResult } from "./process-recovery";

export type SandboxExec = (sandboxName: string, command: string) => SandboxCommandResult | null;
Expand All @@ -29,17 +30,25 @@ const WEDGE_LOG_SIGNATURE =
// strip terminal control characters (no escape-sequence forgery in operator
// terminals) and redact common credential shapes before printing.
const CONTROL_CHARS_RE = new RegExp("[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f]", "g");
const SECRET_PATTERNS: RegExp[] = [
/\b(authorization\s*:\s*bearer)\s+\S+/gi,
/\b(api[-_]?key|token|secret|password)(["']?\s*[=:]\s*["']?)\S+/gi,
/\bnvapi-\S+/gi,
];
// These local patterns run after redactFull and stay: the `nvapi-\S+` catch-all
// is unbounded where the shared one requires ten or more characters.
const AUTHORIZATION_PATTERN = /\b(authorization\s*:\s*bearer)\s+\S+/gi;
const LOCAL_SECRET_ASSIGNMENT_PATTERN =
/\b(api[-_]?key|token|secret|password)(["']?\s*[=:]\s*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\S+)/gi;
const NVAPI_PATTERN = /\bnvapi-\S+/gi;

export function sanitizeWedgeLogLine(line: string): string {
let sanitized = line.replace(CONTROL_CHARS_RE, "");
sanitized = sanitized.replace(SECRET_PATTERNS[0], "$1 [REDACTED]");
sanitized = sanitized.replace(SECRET_PATTERNS[1], "$1$2[REDACTED]");
sanitized = sanitized.replace(SECRET_PATTERNS[2], "[REDACTED]");
sanitized = redactFull(sanitized);
sanitized = sanitized.replace(AUTHORIZATION_PATTERN, "$1 [REDACTED]");
sanitized = sanitized.replace(
LOCAL_SECRET_ASSIGNMENT_PATTERN,
(_match: string, key: string, separator: string, value: string) => {
const quote = value.startsWith('"') ? '"' : value.startsWith("'") ? "'" : "";
return `${key}${separator}${quote}[REDACTED]${quote}`;
},
);
sanitized = sanitized.replace(NVAPI_PATTERN, "[REDACTED]");
return sanitized.trim();
}

Expand Down
28 changes: 28 additions & 0 deletions src/lib/security/redact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ describe("redactForLog", () => {
expect(redactForLog(assignment)).toBe(expected);
});

it.each([
[
'CUSTOM_API_KEY="opaque api key value" safe diagnostic',
'CUSTOM_API_KEY="<REDACTED>" safe diagnostic',
],
[
"CUSTOM_TOKEN='opaque token value' safe diagnostic",
"CUSTOM_TOKEN='<REDACTED>' safe diagnostic",
],
[
'CUSTOM_PASSWORD="opaque password value" safe diagnostic',
'CUSTOM_PASSWORD="<REDACTED>" safe diagnostic',
],
[
"CUSTOM_SECRET='opaque secret value' safe diagnostic",
"CUSTOM_SECRET='<REDACTED>' safe diagnostic",
],
])("redacts the complete quoted assignment in %s", (assignment, expected) => {
expect(redactFull(assignment)).toBe(expected);
expect(redactForLog(assignment)).toBe(expected);
});

it("redacts a quoted multiword sensitive environment assignment before its suffix", () => {
expect(redactSensitiveText('OPENAI_API_KEY="opaque first second" safe diagnostic')).toBe(
"OPENAI_API_KEY=<REDACTED> safe diagnostic",
);
});

it("preserves benign structured keys containing pass", () => {
const benign = {
compass: "north",
Expand Down
19 changes: 18 additions & 1 deletion src/lib/security/redact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ const SENSITIVE_ENV_ASSIGNMENT_KEYS = [
...listMessagingCredentialMetadata().map((credential) => credential.providerEnvKey),
];

const SENSITIVE_ENV_ASSIGNMENT_VALUE = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\S+)`;
const SENSITIVE_ENV_ASSIGNMENT_PATTERN = new RegExp(
`(${SENSITIVE_ENV_ASSIGNMENT_KEYS.map(escapeRegExp).join("|")})=\\S+`,
`(${SENSITIVE_ENV_ASSIGNMENT_KEYS.map(escapeRegExp).join("|")})=${SENSITIVE_ENV_ASSIGNMENT_VALUE}`,
Comment thread
apurvvkumaria marked this conversation as resolved.
Resolved
"gi",
);

Expand Down Expand Up @@ -100,6 +101,19 @@ export function writeRedactedResult(

// ── Full redaction (debug.ts style) ─────────────────────────────

const UNDERSCORE_SECRET_ASSIGNMENT_KEY_SOURCE =
"(?:[A-Za-z0-9]{1,128}_(?:key|token|secret|credential|password|passwd|pass)|(?:x[-_])?api[-_]key|token|secret|credential|password|passwd|pass)";
const CAMEL_SECRET_ASSIGNMENT_KEY_SOURCE =
"(?:[A-Za-z0-9]{1,128}(?:Token|Secret|Credential)|[A-Za-z0-9]{0,128}(?:[Aa]ccess|[Rr]efresh|[Cc]lient|[Bb]earer|[Aa]uth|[Aa][Pp][Ii]|[Pp]rivate|[Ss]igning|[Ss]ession|[Bb]ot|[Aa]pp|[Rr]esolved)Key|[A-Za-z0-9]{1,128}(?:Password|Passwd|Pass))";

function quotedSecretAssignmentPatterns(keySource: string, flags: string): [RegExp, string][] {
const prefix = `((?:^|[^A-Za-z0-9])${keySource}["']?(?:[ \\t]{0,32}[=:][ \\t]{0,32}|[ \\t]{1,32}))`;
return [
[new RegExp(`${prefix}"(?:\\\\.|[^"\\\\])*"`, flags), '$1"<REDACTED>"'],
[new RegExp(`${prefix}'(?:\\\\.|[^'\\\\])*'`, flags), "$1'<REDACTED>'"],
];
}

const FULL_REDACT_PATTERNS: [RegExp, string][] = [
...SECRET_BLOCK_PATTERNS.map((p): [RegExp, string] => [
new RegExp(p.source, p.flags),
Expand Down Expand Up @@ -142,6 +156,9 @@ const FULL_REDACT_PATTERNS: [RegExp, string][] = [
"$1 <REDACTED>",
],
[/(\b(?:cookie|set-cookie)[ \t]*[:=][ \t]*)[^\r\n]*/gi, "$1<REDACTED>"],
...quotedSecretAssignmentPatterns(UNDERSCORE_SECRET_ASSIGNMENT_KEY_SOURCE, "gi"),
...quotedSecretAssignmentPatterns(CAMEL_SECRET_ASSIGNMENT_KEY_SOURCE, "g"),
...quotedSecretAssignmentPatterns("KEY", "g"),
[
/((?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]{1,128}_(?:key|token|secret|credential|password|passwd|pass)|(?:x[-_])?api[-_]key|token|secret|credential|password|passwd|pass)["']?(?:[ \t]{0,32}[=:][ \t]{0,32}|[ \t]{1,32})["']?)[^\s'"]+((?:"|')?)/gi,
"$1<REDACTED>$2",
Expand Down
Loading