From dd61f0df2ca666ccdaaf8770e431c0608619233b Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:10:46 -0700 Subject: [PATCH 1/6] fix(sdk): redact standalone keys and structured credentials --- sdk/typescript/src/errors.ts | 23 ++++++++---- sdk/typescript/tests-ts/errors.test.ts | 50 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 sdk/typescript/tests-ts/errors.test.ts diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index a8face06..ccb45fa0 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -3,17 +3,26 @@ import { formatUsd, type ScanCost } from "./cost.js"; /** Returns an error message with credential-shaped substrings redacted. */ export function redactedErrorMessage(error: unknown): string { const message = error instanceof Error ? error.message : String(error); - const withoutPrivateKeys = message.replaceAll( - /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----(?:\\?["'])?|$)/giu, - "$1[redacted]", - ); + const withoutPrivateKeys = message + .replaceAll( + /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----(?:\\?["'])?|$)/giu, + "$1[redacted]", + ) + .replaceAll( + /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/giu, + "[redacted]", + ); return redactQuotedCredentialValues(withoutPrivateKeys) + .replaceAll( + /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)(?=[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=)[^\r\n]+/giu, + "$1$2$3[redacted]", + ) .replaceAll( /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)(?!\[redacted\]|(?!key\s*=)[A-Za-z_][A-Za-z0-9_-]{0,64}\s*[:=]\s*(?=[^=\s"',;}&\\\]]))[^\s"',;}&\\\]]+/giu, "$1$2$3[redacted]", ) .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?key|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credential|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])[^\s"',;}&\\\]]+/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])[^\s"',;}&\\\]]+/giu, "$1[redacted]", ) .replaceAll(/sk-(?:proj-)?[A-Za-z0-9_*=-]{8,}/gu, "[redacted]") @@ -25,14 +34,14 @@ export function redactedErrorMessage(error: unknown): string { ) .replaceAll(/((?:https?|ssh|git\+ssh):\/\/)[^\s/@]+@/giu, "$1[redacted]@") .replaceAll( - /((?:[?&]|%3F|%26)(?:(?!%3F|%26|%3D)(?:[A-Za-z0-9_.%-]|\[|\])){0,64}(?:api[_-]?key|access(?:[_-]|%5F|%2D)?key(?:(?:[_-]|%5F|%2D)?id)?|private(?:[_-]|%5F|%2D)?key|authorization|auth|token|secret|credential|signature|sig|password|passwd)(?:(?:[_-]|%5F|%2D)[A-Za-z0-9_.%-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_.%-]{0,48})?(?:\]|%5D)?(?:=|%3D))(?:(?!%26)[^&\s])+/giu, + /((?:[?&]|%3F|%26)(?:(?!%3F|%26|%3D)(?:[A-Za-z0-9_.%-]|\[|\])){0,64}(?:api(?:[_-]|%5F|%2D)?keys?|access(?:[_-]|%5F|%2D)?key(?:(?:[_-]|%5F|%2D)?id)?|private(?:[_-]|%5F|%2D)?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:(?:[_-]|%5F|%2D)[A-Za-z0-9_.%-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_.%-]{0,48})?(?:\]|%5D)?(?:=|%3D))(?:(?!%26)[^&\s])+/giu, "$1[redacted]", ); } function redactQuotedCredentialValues(message: string): string { const assignment = - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?key|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credential|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)(\\*)(["'])/giu; + /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)(\\*)(["'])/giu; let output = ""; let consumed = 0; for ( diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts new file mode 100644 index 00000000..4b165f18 --- /dev/null +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { redactedErrorMessage } from "../src/errors.js"; + +describe("security error redaction", () => { + test("redacts standalone and truncated PEM private-key envelopes", () => { + expect( + redactedErrorMessage( + "provider failed: -----BEGIN PRIVATE KEY-----\nSYNTHETIC_PRIVATE_KEY\n-----END PRIVATE KEY----- safe=value", + ), + ).toBe("provider failed: [redacted] safe=value"); + expect( + redactedErrorMessage( + "provider failed: -----BEGIN RSA PRIVATE KEY-----\nSYNTHETIC_TRUNCATED_KEY", + ), + ).toBe("provider failed: [redacted]"); + }); + + test("redacts every parameter from structured authorization schemes", () => { + expect( + redactedErrorMessage( + 'Authorization: Digest username="example", response=SYNTHETIC_DIGEST_SECRET', + ), + ).toBe("Authorization: Digest [redacted]"); + expect( + redactedErrorMessage("auth=Custom response=SYNTHETIC_AUTH_SECRET"), + ).toBe("auth=Custom [redacted]"); + }); + + test("redacts encoded API-key names without consuming other parameters", () => { + for (const separator of ["%5F", "%2D"]) { + const value = `https://example.test/?api${separator}key%3DSYNTHETIC_API_KEY%26safe%3Dvisible`; + expect(redactedErrorMessage(value)).toBe( + `https://example.test/?api${separator}key%3D[redacted]%26safe%3Dvisible`, + ); + } + }); + + test("redacts plural credential assignments and quoted values", () => { + expect( + redactedErrorMessage( + "credentials=SYNTHETIC_CREDENTIAL clientCredentials=SYNTHETIC_CLIENT apiKeys=SYNTHETIC_KEYS", + ), + ).toBe( + "credentials=[redacted] clientCredentials=[redacted] apiKeys=[redacted]", + ); + expect( + redactedErrorMessage('{"credentials":"correct horse battery staple"}'), + ).toBe('{"credentials":"[redacted]"}'); + }); +}); From 552c382ae40f4474b248fa09eb84da53a6f8b772 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:14:10 -0700 Subject: [PATCH 2/6] fix(sdk): preserve diagnostics after structured auth headers --- sdk/typescript/src/errors.ts | 2 +- sdk/typescript/tests-ts/errors.test.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index ccb45fa0..c6178159 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -14,7 +14,7 @@ export function redactedErrorMessage(error: unknown): string { ); return redactQuotedCredentialValues(withoutPrivateKeys) .replaceAll( - /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)(?=[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=)[^\r\n]+/giu, + /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+))*/giu, "$1$2$3[redacted]", ) .replaceAll( diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index 4b165f18..7edaaa69 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -24,6 +24,11 @@ describe("security error redaction", () => { expect( redactedErrorMessage("auth=Custom response=SYNTHETIC_AUTH_SECRET"), ).toBe("auth=Custom [redacted]"); + expect( + redactedErrorMessage( + "Authorization: Custom key=SYNTHETIC_AUTH_SECRET https://example.test/safe", + ), + ).toBe("Authorization: Custom [redacted] https://example.test/safe"); }); test("redacts encoded API-key names without consuming other parameters", () => { From e8b9589bc3e4bbead1c76e4d47966747362b9460 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:24:52 -0700 Subject: [PATCH 3/6] fix(sdk): redact structured auth and credential collections --- sdk/typescript/src/errors.ts | 45 ++++++++++++++++++++++++-- sdk/typescript/tests-ts/errors.test.ts | 25 ++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index c6178159..a06a43ee 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -12,9 +12,11 @@ export function redactedErrorMessage(error: unknown): string { /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/giu, "[redacted]", ); - return redactQuotedCredentialValues(withoutPrivateKeys) + return redactQuotedCredentialValues( + redactCredentialCollections(withoutPrivateKeys), + ) .replaceAll( - /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+))*/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9.+~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+))*/giu, "$1$2$3[redacted]", ) .replaceAll( @@ -39,6 +41,45 @@ export function redactedErrorMessage(error: unknown): string { ); } +function redactCredentialCollections(message: string): string { + const assignment = + /(\b[A-Za-z0-9_-]{0,64}(?:credentials|api[_-]?keys)\b(?:\\*["'])?\s*[:=]\s*)([\[{])/giu; + let output = ""; + let consumed = 0; + for ( + let match = assignment.exec(message); + match !== null; + match = assignment.exec(message) + ) { + const closings = [match[2] === "[" ? "]" : "}"]; + let quote: string | undefined; + let position = assignment.lastIndex; + while (position < message.length && closings.length > 0) { + const character = message[position]!; + if (quote !== undefined) { + if (character === "\\") position += 1; + else if (character === quote) quote = undefined; + } else if (character === '"' || character === "'") { + quote = character; + } else if (character === "[") { + closings.push("]"); + } else if (character === "{") { + closings.push("}"); + } else if (character === closings.at(-1)) { + closings.pop(); + } + position += 1; + } + + const prefix = match[1]!; + const replacement = /:\s*$/u.test(prefix) ? '"[redacted]"' : "[redacted]"; + output += `${message.slice(consumed, match.index)}${prefix}${replacement}`; + consumed = closings.length === 0 ? position : message.length; + assignment.lastIndex = consumed; + } + return output + message.slice(consumed); +} + function redactQuotedCredentialValues(message: string): string { const assignment = /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)(\\*)(["'])/giu; diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index 7edaaa69..e6ce41d5 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -29,6 +29,21 @@ describe("security error redaction", () => { "Authorization: Custom key=SYNTHETIC_AUTH_SECRET https://example.test/safe", ), ).toBe("Authorization: Custom [redacted] https://example.test/safe"); + expect( + redactedErrorMessage( + 'Authorization: Digest username="a\\"b", response="SYNTHETIC_DIGEST_SECRET"', + ), + ).toBe("Authorization: Digest [redacted]"); + expect( + redactedErrorMessage( + "Authorization: Digest username*=UTF-8''user, response=SYNTHETIC_DIGEST_SECRET", + ), + ).toBe("Authorization: Digest [redacted]"); + expect( + redactedErrorMessage( + 'client_authorization_value=Digest username="example", response="SYNTHETIC_DIGEST_SECRET"', + ), + ).toBe("client_authorization_value=Digest [redacted]"); }); test("redacts encoded API-key names without consuming other parameters", () => { @@ -51,5 +66,15 @@ describe("security error redaction", () => { expect( redactedErrorMessage('{"credentials":"correct horse battery staple"}'), ).toBe('{"credentials":"[redacted]"}'); + expect( + redactedErrorMessage( + "credentials=[alice:SYNTHETIC_ONE,bob:SYNTHETIC_TWO] safe=visible", + ), + ).toBe("credentials=[redacted] safe=visible"); + expect( + redactedErrorMessage( + '{"credentials":["SYNTHETIC_ONE","SYNTHETIC_TWO"],"safe":"visible"}', + ), + ).toBe('{"credentials":"[redacted]","safe":"visible"}'); }); }); From bf34227cb2aee2fe9467bf54e6e0b6fc6628a88e Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:37:02 -0700 Subject: [PATCH 4/6] fix(sdk): redact suffixed credential collections --- sdk/typescript/src/errors.ts | 2 +- sdk/typescript/tests-ts/errors.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index a06a43ee..85b0e5a0 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -43,7 +43,7 @@ export function redactedErrorMessage(error: unknown): string { function redactCredentialCollections(message: string): string { const assignment = - /(\b[A-Za-z0-9_-]{0,64}(?:credentials|api[_-]?keys)\b(?:\\*["'])?\s*[:=]\s*)([\[{])/giu; + /(\b[A-Za-z0-9_-]{0,64}(?:credentials|api[_-]?keys)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)([\[{])/giu; let output = ""; let consumed = 0; for ( diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index e6ce41d5..686b44a3 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -76,5 +76,15 @@ describe("security error redaction", () => { '{"credentials":["SYNTHETIC_ONE","SYNTHETIC_TWO"],"safe":"visible"}', ), ).toBe('{"credentials":"[redacted]","safe":"visible"}'); + expect( + redactedErrorMessage( + 'credentials_value=["SYNTHETIC_ONE","SYNTHETIC_TWO"] safe=visible', + ), + ).toBe("credentials_value=[redacted] safe=visible"); + expect( + redactedErrorMessage( + '{"api_keys_data":[{"key":"SYNTHETIC_ONE"},{"key":"SYNTHETIC_TWO"}]}', + ), + ).toBe('{"api_keys_data":"[redacted]"}'); }); }); From c2d786be76254c158c329634c2e9925bc0bfe937 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:53:08 -0700 Subject: [PATCH 5/6] fix(sdk): close credential redaction review edge cases --- sdk/typescript/src/errors.ts | 8 ++++---- sdk/typescript/tests-ts/errors.test.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index 85b0e5a0..4a550e3f 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -5,18 +5,18 @@ export function redactedErrorMessage(error: unknown): string { const message = error instanceof Error ? error.message : String(error); const withoutPrivateKeys = message .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----(?:\\?["'])?|$)/giu, + /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----(?:\\?["'])?|$)/giu, "$1[redacted]", ) .replaceAll( - /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/giu, + /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/giu, "[redacted]", ); return redactQuotedCredentialValues( redactCredentialCollections(withoutPrivateKeys), ) .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9.+~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+))*/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z0-9][A-Za-z0-9.+~-]{0,63})((?:\s|%20|\+)+)[!#$%&'*+.^_`|~0-9A-Za-z-]{1,65}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)(?:\s*,\s*[!#$%&'*+.^_`|~0-9A-Za-z-]{1,65}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+))*/giu, "$1$2$3[redacted]", ) .replaceAll( @@ -24,7 +24,7 @@ export function redactedErrorMessage(error: unknown): string { "$1$2$3[redacted]", ) .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])[^\s"',;}&\\\]]+/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z0-9][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])(?:\\(?!["'])|[^\s"',;}&\\\]])+/giu, "$1[redacted]", ) .replaceAll(/sk-(?:proj-)?[A-Za-z0-9_*=-]{8,}/gu, "[redacted]") diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index 686b44a3..a48ec914 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -13,6 +13,11 @@ describe("security error redaction", () => { "provider failed: -----BEGIN RSA PRIVATE KEY-----\nSYNTHETIC_TRUNCATED_KEY", ), ).toBe("provider failed: [redacted]"); + expect( + redactedErrorMessage( + "provider failed: -----BEGIN PGP PRIVATE KEY BLOCK-----\nSYNTHETIC_PGP_PRIVATE_KEY\n-----END PGP PRIVATE KEY BLOCK----- safe=value", + ), + ).toBe("provider failed: [redacted] safe=value"); }); test("redacts every parameter from structured authorization schemes", () => { @@ -44,6 +49,11 @@ describe("security error redaction", () => { 'client_authorization_value=Digest username="example", response="SYNTHETIC_DIGEST_SECRET"', ), ).toBe("client_authorization_value=Digest [redacted]"); + expect( + redactedErrorMessage( + "Authorization: 0-custom 0nonce=visible, !response=SYNTHETIC_DIGEST_SECRET", + ), + ).toBe("Authorization: 0-custom [redacted]"); }); test("redacts encoded API-key names without consuming other parameters", () => { @@ -86,5 +96,8 @@ describe("security error redaction", () => { '{"api_keys_data":[{"key":"SYNTHETIC_ONE"},{"key":"SYNTHETIC_TWO"}]}', ), ).toBe('{"api_keys_data":"[redacted]"}'); + expect( + redactedErrorMessage("password=prefix\\SYNTHETIC_SECRET safe=visible"), + ).toBe("password=[redacted] safe=visible"); }); }); From 68b11bfd4c498d1b233dafe2051dd84e9acc4afd Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 10 Aug 2026 03:10:28 -0700 Subject: [PATCH 6/6] refactor(sdk): remove local credential redaction --- README.md | 7 +- SECURITY.md | 3 + sdk/typescript/README.md | 10 +- sdk/typescript/src/api.ts | 6 +- sdk/typescript/src/cli.ts | 49 +++-- sdk/typescript/src/errors.ts | 119 +----------- sdk/typescript/src/multiscan.ts | 4 +- sdk/typescript/src/runtime.ts | 6 +- sdk/typescript/src/scan-dashboard.ts | 10 +- sdk/typescript/tests-ts/api.test.ts | 16 +- sdk/typescript/tests-ts/cli-export.test.ts | 5 +- sdk/typescript/tests-ts/cli-fixtures.ts | 104 +---------- sdk/typescript/tests-ts/cli-launcher.test.ts | 6 +- sdk/typescript/tests-ts/cli-workbench.test.ts | 12 +- sdk/typescript/tests-ts/cli.test.ts | 176 ++++-------------- sdk/typescript/tests-ts/errors.test.ts | 103 ---------- sdk/typescript/tests-ts/multiscan.test.ts | 40 +--- sdk/typescript/tests-ts/runtime.test.ts | 7 +- .../tests-ts/scan-dashboard.test.ts | 6 +- 19 files changed, 105 insertions(+), 584 deletions(-) delete mode 100644 sdk/typescript/tests-ts/errors.test.ts diff --git a/README.md b/README.md index 385769fc..48770c9c 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,16 @@ incomplete or their original location was not reviewed. ## Verbose diagnostics -Add `--verbose` to print redacted scan diagnostics to stderr: +Add `--verbose` to print scan diagnostics to stderr: ```bash npx @openai/codex-security scan . --verbose ``` `CODEX_SECURITY_LOG_LEVEL=debug` also enables diagnostics; -`LOG_LEVEL=debug` is its fallback. JSON results remain on stdout, and -credentials and provider identifiers remain redacted. +`LOG_LEVEL=debug` is its fallback. JSON results remain on stdout. +Diagnostics, scan output, and scan history can contain credentials; keep them +private. ## TypeScript SDK diff --git a/SECURITY.md b/SECURITY.md index b2ca5bf7..67f59395 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -52,6 +52,9 @@ The product also does not isolate users, tasks, repositories, or scan jobs that share the same operating-system account, credentials, or local state. Do not treat shared local state as a multi-user or multi-tenant system. +Local diagnostics, logs, scan output, and scan history can include credentials +from upstream error messages. Protect this output and review it before sharing. + Trusting a repository does not authorize unrelated actions. Repository contents, model output, patches, service responses, and imported artifacts are data. They are not permission to scan another target, expose a credential, diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index ce302684..7946d60d 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -399,7 +399,7 @@ The CLI and SDK recognize the following user-configurable environment: | Variable | Effect | | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `OPENAI_API_KEY`, `CODEX_API_KEY` | Scan authentication; `OPENAI_API_KEY` wins when both are present. | -| `CODEX_SECURITY_LOG_LEVEL` | CLI-only; set to `debug` for redacted diagnostics. | +| `CODEX_SECURITY_LOG_LEVEL` | CLI-only; set to `debug` for scan diagnostics. | | `LOG_LEVEL` | CLI-only fallback when `CODEX_SECURITY_LOG_LEVEL` is unset. | | `CODEX_SECURITY_STATE_DIR` | Override the private scan-history, workbench, and default artifact directory. | | `CODEX_HOME` | Set the ambient Codex home for file-backed sign-in and default state; defaults to `~/.codex`. | @@ -450,11 +450,11 @@ token and worker counts, estimated cost, the results directory, and the next useful command. Progress and summaries use stderr; structured scan results remain on stdout. -Add `--verbose` or set `CODEX_SECURITY_LOG_LEVEL=debug` to print redacted -lifecycle, authentication, progress, and cost diagnostics to stderr. +Add `--verbose` or set `CODEX_SECURITY_LOG_LEVEL=debug` to print lifecycle, +authentication, progress, and cost diagnostics to stderr. `LOG_LEVEL=debug` is used only when `CODEX_SECURITY_LOG_LEVEL` is unset. -Credentials and provider identifiers remain redacted, and structured JSON -results remain on stdout. +Structured JSON results remain on stdout. Diagnostics, scan output, and scan +history can contain credentials; keep them private. Each scan records its model, tokens, and estimated cost in its JSON result, scan history, and bulk-scan receipt. Estimates use diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index e11352e9..7aaf8e21 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -48,7 +48,7 @@ import { OutputDirectoryError, OutputInsideProtectedRootError, type ProtectedScanPathKind, - redactedErrorMessage, + errorMessage, ScanCostLimitExceededError, ScanInterruptedError, } from "./errors.js"; @@ -1081,10 +1081,8 @@ export class CodexSecurity { "fail-scan", "--scan-id", activeScan.id, - // Redact before truncating: the stored message is read back by - // `scans show` and travels inside the results directory. "--message", - redactedErrorMessage(failure).slice(0, 2400), + errorMessage(failure).slice(0, 2400), ...(snapshot?.cost ? ["--cost-json", JSON.stringify(snapshot.cost)] : []), diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index a97868fb..8f9ef8ba 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -65,7 +65,7 @@ import { OutputDirectoryError, OutputInsideProtectedRootError, PluginPythonUnavailableError, - redactedErrorMessage, + errorMessage, ScanCostLimitExceededError, ScanInterruptedError, } from "./errors.js"; @@ -730,7 +730,7 @@ export async function main( try { return await select(await dependencies.runWorkbench(args)); } catch (error) { - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); exitCode = 2; return undefined; } @@ -936,7 +936,7 @@ export async function main( scanArguments = scanArgumentsFromRecipe(recipe, args.scanId); scanArguments.verbose = options.verbose; } catch (error) { - const message = redactedErrorMessage(error); + const message = errorMessage(error); errorOutput.write(`codex-security: ${message}\n`); exitCode = 2; return incurError({ @@ -999,7 +999,7 @@ export async function main( format, ); } catch (error) { - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); exitCode = 2; return undefined; } @@ -1052,7 +1052,7 @@ export async function main( verbose: z .boolean() .default(false) - .describe("Print redacted scan diagnostics to stderr."), + .describe("Print scan diagnostics to stderr."), path: z .array(optionValue("--path")) .default([]) @@ -1313,7 +1313,7 @@ export async function main( failOnSeverity: options.failOnSeverity, }; } catch (error) { - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); exitCode = 2; return undefined; } @@ -1502,7 +1502,7 @@ export async function main( onProgress: ({ repository, status, attempt, error, warning }) => { const detail = error ?? warning; errorOutput.write( - `codex-security: ${repository} ${status} (attempt ${attempt})${detail === undefined ? "" : `: ${redactedErrorMessage(detail)}`}\n`, + `codex-security: ${repository} ${status} (attempt ${attempt})${detail === undefined ? "" : `: ${errorMessage(detail)}`}\n`, ); }, }); @@ -1516,7 +1516,7 @@ export async function main( (error instanceof Error && error.name === "ExitPromptError" ? 130 : 2); - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); } finally { dependencies.removeSignalListener("SIGINT", onInterrupt); dependencies.removeSignalListener("SIGTERM", onTerminate); @@ -1620,7 +1620,7 @@ export async function main( ); } catch (error) { exitCode = 2; - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); } }, }) @@ -1656,7 +1656,7 @@ export async function main( ); } catch (error) { exitCode = 2; - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); } }, }) @@ -1845,7 +1845,7 @@ export async function main( if (frameworkExit !== undefined) { if (exitCode !== 0) return exitCode; errorOutput.write( - `codex-security: ${redactedErrorMessage(incurErrorMessage(frameworkOutput))}\n`, + `codex-security: ${errorMessage(incurErrorMessage(frameworkOutput))}\n`, ); return 2; } @@ -1854,7 +1854,7 @@ export async function main( await writeCliOutput(output, renderedHistory ?? frameworkOutput); return exitCode; } catch (error) { - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); return 2; } } @@ -2651,7 +2651,7 @@ async function runExport( } return 0; } catch (error) { - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); return 2; } } @@ -2659,7 +2659,7 @@ async function runExport( type VerboseDiagnosticValue = string | number | boolean | null | undefined; function sanitizeDiagnosticValue(value: unknown): string { - return redactedErrorMessage(value) + return errorMessage(value) .replaceAll( /(\b(?:tenant(?:[_-]?id)?|org(?:anization)?(?:[_-]?id)?|project(?:[_-]?id)?|(?:x[_-]?)?(?:request|trace|correlation)[_-]?id)\b(?:\\*["'])?\s*[:=]\s*)(?!\[redacted\])(?:(\\*)(['"])(?:(?!(? displayed.length ? `, +${arguments_.paths.length - displayed.length} more` : ""}`; } @@ -3477,7 +3474,7 @@ function printScanSummary( ? 33 : 36; errorOutput.write( - `\n ${paint("REPORT", "1;36")} ${paint(redactedErrorMessage(result.reportPath), 4)}\n\n` + + `\n ${paint("REPORT", "1;36")} ${paint(errorMessage(result.reportPath), 4)}\n\n` + ` ${paint("FINDINGS", 1)} ${paint(`${findingCount}${severitySummary === "" ? "" : ` (${severitySummary})`}`, findingColor)}\n` + ` ${paint("COVERAGE", 1)} ${result.coverage.completeness}\n` + ` ${paint("ELAPSED", 1)} ${duration}\n`, @@ -3493,7 +3490,7 @@ function printScanSummary( ); } errorOutput.write( - ` ${paint("RESULTS", 1)} ${redactedErrorMessage(result.scanDir)}\n`, + ` ${paint("RESULTS", 1)} ${errorMessage(result.scanDir)}\n`, ); } @@ -3835,7 +3832,7 @@ function interruptedExit( errorOutput.write( scanDir === null ? "codex-security: No partial output was kept.\n" - : `codex-security: Partial output was kept at ${redactedErrorMessage(scanDir)}.\n`, + : `codex-security: Partial output was kept at ${errorMessage(scanDir)}.\n`, ); return ctrlC ? 130 : 143; } @@ -3861,7 +3858,7 @@ if (invokedAsMain()) { process.exitCode = exitCode; }, (error: unknown) => { - process.stderr.write(`codex-security: ${redactedErrorMessage(error)}\n`); + process.stderr.write(`codex-security: ${errorMessage(error)}\n`); process.exitCode = 2; }, ); diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index 4a550e3f..132d36ce 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -1,122 +1,7 @@ import { formatUsd, type ScanCost } from "./cost.js"; -/** Returns an error message with credential-shaped substrings redacted. */ -export function redactedErrorMessage(error: unknown): string { - const message = error instanceof Error ? error.message : String(error); - const withoutPrivateKeys = message - .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----(?:\\?["'])?|$)/giu, - "$1[redacted]", - ) - .replaceAll( - /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/giu, - "[redacted]", - ); - return redactQuotedCredentialValues( - redactCredentialCollections(withoutPrivateKeys), - ) - .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z0-9][A-Za-z0-9.+~-]{0,63})((?:\s|%20|\+)+)[!#$%&'*+.^_`|~0-9A-Za-z-]{1,65}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)(?:\s*,\s*[!#$%&'*+.^_`|~0-9A-Za-z-]{1,65}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+))*/giu, - "$1$2$3[redacted]", - ) - .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)(?!\[redacted\]|(?!key\s*=)[A-Za-z_][A-Za-z0-9_-]{0,64}\s*[:=]\s*(?=[^=\s"',;}&\\\]]))[^\s"',;}&\\\]]+/giu, - "$1$2$3[redacted]", - ) - .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z0-9][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])(?:\\(?!["'])|[^\s"',;}&\\\]])+/giu, - "$1[redacted]", - ) - .replaceAll(/sk-(?:proj-)?[A-Za-z0-9_*=-]{8,}/gu, "[redacted]") - .replaceAll(/(?:github_pat_|gh[pousr]_)[A-Za-z0-9_-]{8,}/giu, "[redacted]") - .replaceAll(/npm_[A-Za-z0-9_-]{8,}/giu, "[redacted]") - .replaceAll( - /(^|%20|[^A-Za-z0-9_])(Bearer|Basic|Token)((?:\s|%20|\+)+)[A-Za-z0-9.%_~+/*=-]+/giu, - "$1$2$3[redacted]", - ) - .replaceAll(/((?:https?|ssh|git\+ssh):\/\/)[^\s/@]+@/giu, "$1[redacted]@") - .replaceAll( - /((?:[?&]|%3F|%26)(?:(?!%3F|%26|%3D)(?:[A-Za-z0-9_.%-]|\[|\])){0,64}(?:api(?:[_-]|%5F|%2D)?keys?|access(?:[_-]|%5F|%2D)?key(?:(?:[_-]|%5F|%2D)?id)?|private(?:[_-]|%5F|%2D)?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:(?:[_-]|%5F|%2D)[A-Za-z0-9_.%-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_.%-]{0,48})?(?:\]|%5D)?(?:=|%3D))(?:(?!%26)[^&\s])+/giu, - "$1[redacted]", - ); -} - -function redactCredentialCollections(message: string): string { - const assignment = - /(\b[A-Za-z0-9_-]{0,64}(?:credentials|api[_-]?keys)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)([\[{])/giu; - let output = ""; - let consumed = 0; - for ( - let match = assignment.exec(message); - match !== null; - match = assignment.exec(message) - ) { - const closings = [match[2] === "[" ? "]" : "}"]; - let quote: string | undefined; - let position = assignment.lastIndex; - while (position < message.length && closings.length > 0) { - const character = message[position]!; - if (quote !== undefined) { - if (character === "\\") position += 1; - else if (character === quote) quote = undefined; - } else if (character === '"' || character === "'") { - quote = character; - } else if (character === "[") { - closings.push("]"); - } else if (character === "{") { - closings.push("}"); - } else if (character === closings.at(-1)) { - closings.pop(); - } - position += 1; - } - - const prefix = match[1]!; - const replacement = /:\s*$/u.test(prefix) ? '"[redacted]"' : "[redacted]"; - output += `${message.slice(consumed, match.index)}${prefix}${replacement}`; - consumed = closings.length === 0 ? position : message.length; - assignment.lastIndex = consumed; - } - return output + message.slice(consumed); -} - -function redactQuotedCredentialValues(message: string): string { - const assignment = - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)(\\*)(["'])/giu; - let output = ""; - let consumed = 0; - for ( - let match = assignment.exec(message); - match !== null; - match = assignment.exec(message) - ) { - const openingSlashes = match[2]!.length; - const quote = match[3]!; - let position = assignment.lastIndex; - let closed = false; - while (position < message.length) { - const delimiter = message.indexOf(quote, position); - if (delimiter < 0) break; - let preceding = delimiter; - while (preceding > position && message[preceding - 1] === "\\") { - preceding -= 1; - } - if (delimiter - preceding === openingSlashes) { - output += `${message.slice(consumed, assignment.lastIndex)}[redacted]${message.slice(preceding, delimiter + 1)}`; - consumed = delimiter + 1; - assignment.lastIndex = consumed; - closed = true; - break; - } - position = delimiter + 1; - } - if (!closed) { - output += `${message.slice(consumed, assignment.lastIndex)}[redacted]`; - consumed = message.length; - break; - } - } - return output + message.slice(consumed); +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); } /** Base error for Codex Security SDK failures. */ diff --git a/sdk/typescript/src/multiscan.ts b/sdk/typescript/src/multiscan.ts index 9704014c..ec178cba 100644 --- a/sdk/typescript/src/multiscan.ts +++ b/sdk/typescript/src/multiscan.ts @@ -19,7 +19,7 @@ import Papa from "papaparse"; import type { CodexSecurity } from "./api.js"; import type { CodexSecurityConfig } from "./config.js"; import type { ScanCost } from "./cost.js"; -import { redactedErrorMessage } from "./errors.js"; +import { errorMessage } from "./errors.js"; import type { CoverageDocument } from "./models.js"; import type { ScanMode } from "./targets.js"; import { resolveTrustedExecutable } from "./trusted-executable.js"; @@ -246,7 +246,7 @@ async function runCampaign( } } catch (error) { if (options.signal?.aborted === true) options.signal.throwIfAborted(); - failure = redactedErrorMessage(error); + failure = errorMessage(error); } finally { await rm(checkout, { recursive: true, force: true }); } diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index 476eaacf..293c12a1 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -43,7 +43,6 @@ import { OutputDirectoryError, PluginBootstrapError, PluginPythonUnavailableError, - redactedErrorMessage, } from "./errors.js"; import type { JsonObject } from "./config.js"; import { resolveTrustedExecutable } from "./trusted-executable.js"; @@ -283,10 +282,7 @@ function windowsCredentialAclFailure(error: unknown): string { : error instanceof Error ? error.message : String(error); - const sanitized = redactedErrorMessage(detail) - .replace(/\s+/gu, " ") - .trim() - .slice(0, 512); + const sanitized = detail.replace(/\s+/gu, " ").trim().slice(0, 512); return sanitized === "" ? "" : `. ${sanitized}`; } diff --git a/sdk/typescript/src/scan-dashboard.ts b/sdk/typescript/src/scan-dashboard.ts index 26713d07..293d3a45 100644 --- a/sdk/typescript/src/scan-dashboard.ts +++ b/sdk/typescript/src/scan-dashboard.ts @@ -45,7 +45,6 @@ interface ScanDashboardOptions { maxCostUsd?: number; clock: DashboardClock; color?: boolean; - sanitize?: (value: string) => string; input?: DashboardInput; onInterrupt?: () => void; } @@ -310,10 +309,7 @@ export class ScanDashboard { lines .map((line, index) => { const text = typeof line === "string" ? line : line.text; - const clean = fitLine( - this.#options.sanitize?.(text) ?? text, - width, - ); + const clean = fitLine(text, width); const colored = this.#options.color === true ? styleLine( @@ -333,7 +329,6 @@ export class ScanDashboard { ? styleInlineCode(colored, line.code, line.kind) : colored, line.links, - this.#options.sanitize, ); return `${ERASE_LINE}${formatted}`; }) @@ -449,10 +444,9 @@ function styleInlineCode( function linkActivity( value: string, links: readonly DashboardActivityLink[] | undefined, - sanitize: ((value: string) => string) | undefined, ): string { for (const { label, target } of links ?? []) { - const safe = safeHyperlinkTarget(sanitize?.(target) ?? target); + const safe = safeHyperlinkTarget(target); if (safe !== undefined) { value = value.replace( label, diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 2ea5b331..fcb977f5 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -50,7 +50,7 @@ import { setCodexSecurityCredentialLogout, } from "../src/runtime.js"; import { normalizeTarget } from "../src/targets.js"; -import { REDACTED_CREDENTIALS, SYNTHETIC_CREDENTIALS } from "./cli-fixtures.js"; +import { SYNTHETIC_CREDENTIALS } from "./cli-fixtures.js"; import { INTEGRATION_TARGET, PLUGIN_ROOT } from "./plugin-root.js"; type ScanObserverName = Parameters< @@ -3145,7 +3145,7 @@ describe("CodexSecurity orchestration", () => { await client.close(); }); - test("redacts credentials from the stored scan failure message", async () => { + test("preserves the original stored scan failure message", async () => { const root = await temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -3164,7 +3164,7 @@ describe("CodexSecurity orchestration", () => { const quotedCredential = JSON.stringify({ client_secret_value: "SYNTHETIC correct horse battery staple", }); - const redactedFailure = `${REDACTED_CREDENTIALS} {"client_secret_value":"[redacted]"}`; + const failureMessage = `${SYNTHETIC_CREDENTIALS} ${quotedCredential}`; const client = new TestClient( {}, { @@ -3200,14 +3200,12 @@ describe("CodexSecurity orchestration", () => { }, ); - // The in-memory error keeps its original text; only what leaves the process - // is redacted, so the CLI can still classify the upstream failure. await expect(client.run(repository)).rejects.toThrow(SYNTHETIC_CREDENTIALS); const failure = commands.find((args) => args[0] === "fail-scan"); const scanId = failure?.[2] ?? ""; expect(scanId).toMatch(/^[0-9a-f-]{36}$/); expect(failure?.[3]).toBe("--message"); - expect(failure?.[4]).toBe(redactedFailure); + expect(failure?.[4]).toBe(failureMessage); // `scans show` reads the stored message back through get-scan. const context = await runWorkbench( @@ -3216,13 +3214,11 @@ describe("CodexSecurity orchestration", () => { ); expect(context["scan"]).toMatchObject({ progress: { status: "failed" }, - failureMessage: redactedFailure, + failureMessage, }); - // Every synthetic credential is tagged SYNTHETIC, so the database file - // itself proves nothing was persisted anywhere on the failure path. const database = await readFile(join(stateDirectory, "workbench.sqlite3")); - expect(database.toString("latin1")).not.toContain("SYNTHETIC"); + expect(database.toString("latin1")).toContain(failureMessage); await client.close(); }); diff --git a/sdk/typescript/tests-ts/cli-export.test.ts b/sdk/typescript/tests-ts/cli-export.test.ts index aacfb329..dc784495 100644 --- a/sdk/typescript/tests-ts/cli-export.test.ts +++ b/sdk/typescript/tests-ts/cli-export.test.ts @@ -16,7 +16,6 @@ import { describe, expect, test } from "bun:test"; import { exportEnvironment, main } from "../src/cli.js"; import { CodexSecurityError } from "../src/index.js"; import { - REDACTED_CREDENTIALS, SYNTHETIC_CREDENTIALS, capture, dependencies, @@ -583,7 +582,7 @@ describe("CLI", () => { ); }); - test("redacts credentials from caught export failures", async () => { + test("preserves caught export failures", async () => { const stdout = capture(); const stderr = capture(); const deps = dependencies(); @@ -601,7 +600,7 @@ describe("CLI", () => { ).toBe(2); expect(stdout.text()).toBe(""); expect(stderr.text()).toBe( - `codex-security: export failed ${REDACTED_CREDENTIALS}\n`, + `codex-security: export failed ${SYNTHETIC_CREDENTIALS}\n`, ); }); }); diff --git a/sdk/typescript/tests-ts/cli-fixtures.ts b/sdk/typescript/tests-ts/cli-fixtures.ts index 24040954..644e8db9 100644 --- a/sdk/typescript/tests-ts/cli-fixtures.ts +++ b/sdk/typescript/tests-ts/cli-fixtures.ts @@ -21,109 +21,7 @@ import type { UpdateNotice } from "../src/version.js"; type MainDependencies = NonNullable[3]>; -export const SYNTHETIC_CREDENTIALS = [ - "sk-proj-SYNTHETIC_KEY_123", - "Bearer SYNTHETIC_TOKEN_123", - "Authorization: Basic SYNTHETIC_BASIC_123", - "Authorization: Token SYNTHETIC_HEADER_TOKEN_123", - "Authorization: Bearer%20SYNTHETIC%2FENCODED%2BTOKEN_123", - "Authorization%3A%20Bearer%20SYNTHETIC_FULLY_ENCODED_TOKEN_123", - "https://SYNTHETIC_USER:SYNTHETIC_PASSWORD@example.test/private", - "ssh://SYNTHETIC_USER:SYNTHETIC_SSH_PASSWORD@example.test/private", - "git+ssh://SYNTHETIC_USER:SYNTHETIC_GIT_PASSWORD@example.test/private", - "github_pat_SYNTHETIC_GITHUB_PAT_123", - "ghs_SYNTHETIC_GITHUB_TOKEN_123", - "OPENAI_API_KEY=SYNTHETIC_OPENAI_VALUE_123", - "CODEX_API_KEY=SYNTHETIC_CODEX_VALUE_123", - "CODEX_ACCESS_TOKEN=SYNTHETIC_CODEX_ACCESS_TOKEN_123", - "GITHUB_TOKEN=SYNTHETIC_GITHUB_VALUE_123", - "GH_TOKEN=SYNTHETIC_GH_VALUE_123", - '{"OPENAI_API_KEY":"SYNTHETIC_JSON_OPENAI_123","CODEX_API_KEY":"SYNTHETIC_JSON_CODEX_123"}', - '{\\"OPENAI_API_KEY\\":\\"SYNTHETIC_ESCAPED_OPENAI_123\\",\\"CODEX_API_KEY\\":\\"SYNTHETIC_ESCAPED_CODEX_123\\"}', - '{"refresh_token":"SYNTHETIC_REFRESH_TOKEN_123","id_token":"SYNTHETIC_ID_TOKEN_123","clientSecret":"SYNTHETIC_CLIENT_SECRET_123","dbPassword":"SYNTHETIC_PASSWORD_123","passwd":"SYNTHETIC_PASSWD_123"}', - '{\\"refreshToken\\":\\"SYNTHETIC_ESCAPED_REFRESH_123\\",\\"idToken\\":\\"SYNTHETIC_ESCAPED_ID_123\\",\\"clientSecret\\":\\"SYNTHETIC_ESCAPED_SECRET_123\\",\\"password\\":\\"SYNTHETIC_ESCAPED_PASSWORD_123\\"}', - "AWS_SECRET_ACCESS_KEY=SYNTHETIC_AWS_SECRET_123", - "AWS_ACCESS_KEY_ID=SYNTHETIC_AWS_ID_123", - "AWS_SESSION_TOKEN=SYNTHETIC_AWS_SESSION_123", - "NODE_AUTH_TOKEN=SYNTHETIC_NODE_AUTH_123", - "NPM_TOKEN=SYNTHETIC_NPM_TOKEN_123", - "OPENAI_API_KEY=sk-proj-SYNTHETIC_NAMED_OPENAI_123", - "GITHUB_TOKEN=ghs_SYNTHETIC_NAMED_GITHUB_123", - "NPM_TOKEN=npm_SYNTHETIC_NAMED_NPM_123", - "ACTIONS_ID_TOKEN_REQUEST_TOKEN=SYNTHETIC_ACTIONS_TOKEN_123", - "ACTIONS_RUNTIME_TOKEN=SYNTHETIC_ACTIONS_RUNTIME_123", - "GITLAB_TOKEN=SYNTHETIC_GITLAB_TOKEN_123", - "HF_TOKEN=SYNTHETIC_HF_TOKEN_123", - "SLACK_BOT_TOKEN=SYNTHETIC_SLACK_TOKEN_123", - "//registry.npmjs.org/:_authToken=SYNTHETIC_NPMRC_TOKEN_123", - "x-api-key: SYNTHETIC_HEADER_KEY_123", - "access_token=SYNTHETIC_ACCESS_TOKEN_123", - "npm_SYNTHETIC_BARE_TOKEN_123", - "https://example.test/?token=SYNTHETIC_QUERY_123&safe=1", - "https://example.test/?credential=SYNTHETIC_CREDENTIAL_123&safe=1", - "https://example.test/?AWS_ACCESS_KEY_ID=SYNTHETIC_QUERY_AWS_ID_123&safe=1", - "https://example.test/?AWS%5FACCESS%5FKEY%5FID=SYNTHETIC_ENCODED_AWS_ID_123&AWS%2DACCESS%2DKEY%2DID=SYNTHETIC_ENCODED_AWS_DASH_ID_123&safe=1", - "https://example.test/?service-api-key=SYNTHETIC_QUERY_API_KEY_123&service-access-token=SYNTHETIC_QUERY_ACCESS_TOKEN_123&service-token=SYNTHETIC_QUERY_TOKEN_123&service-secret=SYNTHETIC_QUERY_SECRET_123&signature=SYNTHETIC_SIGNATURE_123&safe=1", - "https://example.test/?X-Amz-Signature=SYNTHETIC_AMZ_SIGNATURE_123&X-Amz-Credential=SYNTHETIC_AMZ_CREDENTIAL_123&X-Amz-Security-Token=SYNTHETIC_AMZ_TOKEN_123&safe=1", - "https://example.test/?X-Goog-Signature=SYNTHETIC_GOOG_SIGNATURE_123&X-Goog-Credential=SYNTHETIC_GOOG_CREDENTIAL_123&safe=1", - "https://example.test/?sv=2026-01-01&sig=SYNTHETIC_AZURE_SIG_123&safe=1", - "https://example.test/?password=SYNTHETIC_QUERY_PASSWORD_123&passwd=SYNTHETIC_QUERY_PASSWD_123&safe=1", - "https://example.test/?oauth.refreshToken=SYNTHETIC_DOTTED_TOKEN_123&auth[token]=SYNTHETIC_BRACKET_TOKEN_123&auth%5BclientSecret%5D=SYNTHETIC_ENCODED_SECRET_123&safe=1", - "https://example.test/?access_token%3DSYNTHETIC_ENCODED_ACCESS_123&client_secret%3DSYNTHETIC_ENCODED_CLIENT_123&safe=1", - "https://example.test/?redirect_uri=https%3A%2F%2Finner.test%2Fcb%3Frefresh_token%3DSYNTHETIC_NESTED_REFRESH_123%26password%3DSYNTHETIC_NESTED_PASSWORD_123%26safe%3D1", -].join(" "); - -export const REDACTED_CREDENTIALS = [ - "[redacted]", - "Bearer [redacted]", - "Authorization: Basic [redacted]", - "Authorization: Token [redacted]", - "Authorization: Bearer%20[redacted]", - "Authorization%3A%20Bearer%20[redacted]", - "https://[redacted]@example.test/private", - "ssh://[redacted]@example.test/private", - "git+ssh://[redacted]@example.test/private", - "[redacted]", - "[redacted]", - "OPENAI_API_KEY=[redacted]", - "CODEX_API_KEY=[redacted]", - "CODEX_ACCESS_TOKEN=[redacted]", - "GITHUB_TOKEN=[redacted]", - "GH_TOKEN=[redacted]", - '{"OPENAI_API_KEY":"[redacted]","CODEX_API_KEY":"[redacted]"}', - '{\\"OPENAI_API_KEY\\":\\"[redacted]\\",\\"CODEX_API_KEY\\":\\"[redacted]\\"}', - '{"refresh_token":"[redacted]","id_token":"[redacted]","clientSecret":"[redacted]","dbPassword":"[redacted]","passwd":"[redacted]"}', - '{\\"refreshToken\\":\\"[redacted]\\",\\"idToken\\":\\"[redacted]\\",\\"clientSecret\\":\\"[redacted]\\",\\"password\\":\\"[redacted]\\"}', - "AWS_SECRET_ACCESS_KEY=[redacted]", - "AWS_ACCESS_KEY_ID=[redacted]", - "AWS_SESSION_TOKEN=[redacted]", - "NODE_AUTH_TOKEN=[redacted]", - "NPM_TOKEN=[redacted]", - "OPENAI_API_KEY=[redacted]", - "GITHUB_TOKEN=[redacted]", - "NPM_TOKEN=[redacted]", - "ACTIONS_ID_TOKEN_REQUEST_TOKEN=[redacted]", - "ACTIONS_RUNTIME_TOKEN=[redacted]", - "GITLAB_TOKEN=[redacted]", - "HF_TOKEN=[redacted]", - "SLACK_BOT_TOKEN=[redacted]", - "//registry.npmjs.org/:_authToken=[redacted]", - "x-api-key: [redacted]", - "access_token=[redacted]", - "[redacted]", - "https://example.test/?token=[redacted]&safe=1", - "https://example.test/?credential=[redacted]&safe=1", - "https://example.test/?AWS_ACCESS_KEY_ID=[redacted]&safe=1", - "https://example.test/?AWS%5FACCESS%5FKEY%5FID=[redacted]&AWS%2DACCESS%2DKEY%2DID=[redacted]&safe=1", - "https://example.test/?service-api-key=[redacted]&service-access-token=[redacted]&service-token=[redacted]&service-secret=[redacted]&signature=[redacted]&safe=1", - "https://example.test/?X-Amz-Signature=[redacted]&X-Amz-Credential=[redacted]&X-Amz-Security-Token=[redacted]&safe=1", - "https://example.test/?X-Goog-Signature=[redacted]&X-Goog-Credential=[redacted]&safe=1", - "https://example.test/?sv=2026-01-01&sig=[redacted]&safe=1", - "https://example.test/?password=[redacted]&passwd=[redacted]&safe=1", - "https://example.test/?oauth.refreshToken=[redacted]&auth[token]=[redacted]&auth%5BclientSecret%5D=[redacted]&safe=1", - "https://example.test/?access_token%3D[redacted]&client_secret%3D[redacted]&safe=1", - "https://example.test/?redirect_uri=https%3A%2F%2Finner.test%2Fcb%3Frefresh_token%3D[redacted]%26password%3D[redacted]%26safe%3D1", -].join(" "); +export const SYNTHETIC_CREDENTIALS = "sk-proj-SYNTHETIC_KEY_123"; export function capture(isTTY = false): { stream: Pick & diff --git a/sdk/typescript/tests-ts/cli-launcher.test.ts b/sdk/typescript/tests-ts/cli-launcher.test.ts index 703256df..92a0e777 100644 --- a/sdk/typescript/tests-ts/cli-launcher.test.ts +++ b/sdk/typescript/tests-ts/cli-launcher.test.ts @@ -13,7 +13,7 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, test } from "bun:test"; import { VERSION } from "../src/index.js"; -import { REDACTED_CREDENTIALS, SYNTHETIC_CREDENTIALS } from "./support/cli.js"; +import { SYNTHETIC_CREDENTIALS } from "./support/cli.js"; const packageRoot = join(import.meta.dir, ".."); @@ -40,7 +40,7 @@ describe("CLI launcher", () => { } }); - test("maps unexpected source-entrypoint failures to exit 2 and redacts credentials", async () => { + test("maps unexpected source-entrypoint failures to exit 2", async () => { const root = await mkdtemp(join(tmpdir(), "codex-security-cli-failure-")); try { const preload = join(root, "unavailable-cwd.mjs"); @@ -57,7 +57,7 @@ describe("CLI launcher", () => { expect(child.status).toBe(2); expect(child.stdout).toBe(""); expect(child.stderr).toBe( - `working directory is unavailable: ${REDACTED_CREDENTIALS}\n`, + `working directory is unavailable: ${SYNTHETIC_CREDENTIALS}\n`, ); } finally { await rm(root, { recursive: true, force: true }); diff --git a/sdk/typescript/tests-ts/cli-workbench.test.ts b/sdk/typescript/tests-ts/cli-workbench.test.ts index aea574ba..5d6c559e 100644 --- a/sdk/typescript/tests-ts/cli-workbench.test.ts +++ b/sdk/typescript/tests-ts/cli-workbench.test.ts @@ -3,12 +3,7 @@ import { describe, expect, test } from "bun:test"; import type { CodexSecurityConfig, JsonObject } from "../src/index.js"; import { DiffTarget } from "../src/index.js"; import { main } from "../src/cli.js"; -import { - capture, - dependencies, - REDACTED_CREDENTIALS, - SYNTHETIC_CREDENTIALS, -} from "./support/cli.js"; +import { capture, dependencies, SYNTHETIC_CREDENTIALS } from "./support/cli.js"; describe("CLI workbench", () => { test("lists repository and scan-root history without starting Codex", async () => { @@ -619,7 +614,7 @@ describe("CLI workbench", () => { } }); - test("redacts workbench failures and does not initialize Codex", async () => { + test("reports workbench failures and does not initialize Codex", async () => { const stderr = capture(); let started = false; expect( @@ -637,8 +632,7 @@ describe("CLI workbench", () => { }), ), ).toBe(2); - expect(stderr.text()).toContain(REDACTED_CREDENTIALS); - expect(stderr.text()).not.toContain("SYNTHETIC_KEY_123"); + expect(stderr.text()).toContain(SYNTHETIC_CREDENTIALS); expect(started).toBe(false); }); }); diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 5475ef49..b726520b 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -22,7 +22,6 @@ import type { ScanOptions, ScanPreflight, } from "../src/index.js"; -import { redactedErrorMessage } from "../src/errors.js"; import { BUNDLED_PLUGIN_VERSION, CodexSecurityError, @@ -45,7 +44,6 @@ import { } from "../src/config.js"; import { FakeSignals, - REDACTED_CREDENTIALS, SYNTHETIC_CREDENTIALS, capture, dependencies, @@ -499,7 +497,7 @@ describe("CLI", () => { expect(workbenchCalled).toBe(false); }); - test("redacts false-positive workbench failures", async () => { + test("reports false-positive workbench failures", async () => { const stdout = capture(); const stderr = capture(); let started = false; @@ -529,8 +527,7 @@ describe("CLI", () => { ), ).toBe(2); expect(stdout.text()).toBe(""); - expect(stderr.text()).toContain(REDACTED_CREDENTIALS); - expect(stderr.text()).not.toContain("SYNTHETIC_KEY_123"); + expect(stderr.text()).toContain(SYNTHETIC_CREDENTIALS); expect(started).toBe(false); }); @@ -837,7 +834,7 @@ describe("CLI", () => { }, ); - test("preserves the bulk-scan failure summary and redacts progress errors", async () => { + test("preserves the bulk-scan failure summary and progress errors", async () => { const root = await mkdtemp(join(tmpdir(), "codex-security-cli-multiscan-")); try { await multiscanInventory(root); @@ -871,8 +868,7 @@ describe("CLI", () => { skipped: 0, }); expect(stderr.text()).toContain("sample failed (attempt 1)"); - expect(stderr.text()).toContain("[redacted]"); - expect(stderr.text()).not.toContain("SYNTHETIC_KEY_123"); + expect(stderr.text()).toContain("SYNTHETIC_KEY_123"); } finally { await rm(root, { recursive: true, force: true }); } @@ -1688,8 +1684,9 @@ describe("CLI", () => { ); expect(text).not.toContain("thinking ·"); expect(text).not.toContain("said ·"); - expect(text).toContain('curl -H "Authorization: Bearer [redacted]"'); - expect(text).not.toContain("SYNTHETIC_OPENAI_VALUE_123"); + expect(text).toContain( + 'curl -H "Authorization: Bearer sk-proj-SYNTHETIC_OPENAI_VALUE_123"', + ); expect(text).not.toContain("Building the file inventory"); expect(text).not.toContain("Running a scan command"); expect(text).toContain("3 / 1,258 reviewed"); @@ -3334,7 +3331,7 @@ describe("CLI", () => { } }); - test("redacts verbose output paths and observer diagnostics", async () => { + test("preserves verbose output paths and observer diagnostics", async () => { const stdout = capture(); const stderr = capture(); const deps = dependencies(); @@ -3366,16 +3363,15 @@ describe("CLI", () => { ).toBe(0); expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); expect(stderr.text()).toContain( - 'codex-security: debug: scan.output_archived archive_dir="/tmp/archive_[redacted]"', + 'codex-security: debug: scan.output_archived archive_dir="/tmp/archive_sk-proj-SYNTHETIC_ARCHIVE_SECRET_123"', ); expect(stderr.text()).toContain( - 'codex-security: debug: scan.output_ready scan_dir="/tmp/scan_[redacted]"', + 'codex-security: debug: scan.output_ready scan_dir="/tmp/scan_sk-proj-SYNTHETIC_OUTPUT_SECRET_123"', ); expect(stderr.text()).toContain( 'codex-security: debug: scan.observer_failed observer="onWorkerStatus"', ); - expect(stderr.text()).toContain("[redacted]"); - expect(stderr.text()).not.toContain("SYNTHETIC"); + expect(stderr.text()).toContain("SYNTHETIC_KEY_123"); }); test("excludes observer failure context from verbose diagnostics", async () => { @@ -3692,7 +3688,7 @@ describe("CLI", () => { } }); - test("redacts credentials in underlying network errors", async () => { + test("preserves underlying network errors", async () => { const stdout = capture(); const stderr = capture(); const deps = dependencies(); @@ -3711,106 +3707,12 @@ describe("CLI", () => { ).toBe(2); expect(stdout.text()).toBe(""); expect(stderr.text()).toContain( - `network failure ECONNRESET ${REDACTED_CREDENTIALS}`, + `network failure ECONNRESET ${SYNTHETIC_CREDENTIALS}`, ); - expect(stderr.text()).not.toContain("SYNTHETIC_KEY_123"); + expect(stderr.text()).toContain("SYNTHETIC_KEY_123"); expect(stderr.text()).not.toContain("model service could not be reached"); }); - test("redacts quoted multiword credentials and private-key assignments", () => { - expect( - redactedErrorMessage( - 'password="correct horse battery staple" private_key=SYNTHETIC_PRIVATE_KEY_123', - ), - ).toBe('password="[redacted]" private_key=[redacted]'); - expect( - redactedErrorMessage( - '{"client_secret_value":"correct horse battery staple","safe":"visible"}', - ), - ).toBe('{"client_secret_value":"[redacted]","safe":"visible"}'); - expect( - redactedErrorMessage( - '{"clientSecretValue":"camel case secret","accessTokenValue":"camel case token"}', - ), - ).toBe( - '{"clientSecretValue":"[redacted]","accessTokenValue":"[redacted]"}', - ); - expect( - redactedErrorMessage( - "clientSecretValue=SYNTHETIC_CAMEL_SECRET accessTokenValue=SYNTHETIC_CAMEL_TOKEN https://example.test/?clientSecretValue=SYNTHETIC_CAMEL_QUERY", - ), - ).toBe( - "clientSecretValue=[redacted] accessTokenValue=[redacted] https://example.test/?clientSecretValue=[redacted]", - ); - expect( - redactedErrorMessage( - '{\\"access_token_value\\":\\"another horse battery staple\\"}', - ), - ).toBe('{\\"access_token_value\\":\\"[redacted]\\"}'); - expect( - redactedErrorMessage( - 'authorization="opaque secret value" _auth=Zm9vOmJhcg== https://example.test/?authorization=opaque%20query%20secret', - ), - ).toBe( - 'authorization="[redacted]" _auth=[redacted] https://example.test/?authorization=[redacted]', - ); - for (const [authorization, redacted] of [ - [ - "Authorization: ApiKey SYNTHETIC_APIKEY_SECRET", - "Authorization: ApiKey [redacted]", - ], - ["auth=Custom%20SYNTHETIC_CUSTOM_SECRET", "auth=Custom%20[redacted]"], - [ - "Authorization: Digest+SYNTHETIC_DIGEST_SECRET", - "Authorization: Digest+[redacted]", - ], - [ - "client_authorization_value=ApiKey SYNTHETIC_SUFFIXED_SECRET", - "client_authorization_value=ApiKey [redacted]", - ], - ["Authorization: ApiKey dGVzdA==", "Authorization: ApiKey [redacted]"], - ["Authorization: ApiKey dGVzdA=", "Authorization: ApiKey [redacted]"], - ["Authorization: ApiKey key=SECRET", "Authorization: ApiKey [redacted]"], - ["auth=Custom key=SECRET", "auth=Custom [redacted]"], - [ - "client_auth_token=Custom dGVzdA==", - "client_auth_token=Custom [redacted]", - ], - ] as const) { - expect(redactedErrorMessage(authorization)).toBe(redacted); - } - expect(redactedErrorMessage('password="correct horse battery staple')).toBe( - 'password="[redacted]', - ); - let encoded: string | { password: string } = { - password: 'foo "bar" baz', - }; - for (let depth = 1; depth <= 3; depth += 1) { - encoded = JSON.stringify(encoded); - const redacted = redactedErrorMessage(encoded); - expect(redacted).not.toContain("foo"); - expect(redacted).not.toContain("bar"); - expect(redacted).not.toContain("baz"); - let decoded: unknown = redacted; - for (let layer = 0; layer < depth; layer += 1) { - decoded = JSON.parse(decoded as string); - } - expect(decoded).toEqual({ password: "[redacted]" }); - } - for (const separator of ["\n", "\\n"]) { - expect( - redactedErrorMessage( - `private_key=-----BEGIN PRIVATE KEY-----${separator}MII_SYNTHETIC_PRIVATE_KEY${separator}-----END PRIVATE KEY----- safe=value`, - ), - ).toBe("private_key=[redacted] safe=value"); - expect( - redactedErrorMessage( - `private_key=-----BEGIN PRIVATE KEY-----${separator}MII_SYNTHETIC_TRUNCATED_PRIVATE_KEY`, - ), - ).toBe("private_key=[redacted]"); - } - }); - test("reports database connection failures without claiming the model network failed", async () => { const stdout = capture(); const stderr = capture(); @@ -3835,7 +3737,7 @@ describe("CLI", () => { expect(stderr.text()).toContain("unable to open database file"); expect(stderr.text()).not.toContain("model service could not be reached"); expect(stderr.text()).not.toContain("Check your network connection"); - expect(stderr.text()).not.toContain("SYNTHETIC_DATABASE_SECRET"); + expect(stderr.text()).toContain("SYNTHETIC_DATABASE_SECRET"); }); test("prints only the completion summary for default scans", async () => { @@ -4020,7 +3922,7 @@ describe("CLI", () => { } }); - test("emits redacted scan warnings in verbose diagnostics", async () => { + test("emits scan warnings in verbose diagnostics", async () => { const stdout = capture(); const stderr = capture(); const deps = dependencies(); @@ -4045,12 +3947,11 @@ describe("CLI", () => { ).toBe(0); expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); expect(stderr.text()).toContain( - 'codex-security: debug: scan.warning message="Repository HEAD changed during the scan: [redacted]"', + 'codex-security: debug: scan.warning message="Repository HEAD changed during the scan: sk-proj-SYNTHETIC_WARNING_SECRET_123"', ); expect(stderr.text()).toContain( - "codex-security: warning: Repository HEAD changed during the scan: [redacted]", + "codex-security: warning: Repository HEAD changed during the scan: sk-proj-SYNTHETIC_WARNING_SECRET_123", ); - expect(stderr.text()).not.toContain("SYNTHETIC_WARNING_SECRET"); }); test("prints granted trusted cyber access without warning or corrupting JSON scans", async () => { @@ -4175,9 +4076,8 @@ describe("CLI", () => { ).toBe(0); expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); expect(stderr.text()).toContain( - `codex-security: warning: onWorkerStatus observer failed: status observer failed ${REDACTED_CREDENTIALS}`, + `codex-security: warning: onWorkerStatus observer failed: status observer failed ${SYNTHETIC_CREDENTIALS}`, ); - expect(stderr.text()).not.toContain("SYNTHETIC_OPENAI_VALUE_123"); }); test("maps failed scan stdout writes to the runtime-error exit code", async () => { @@ -4481,7 +4381,7 @@ describe("CLI", () => { expect(JSON.parse(stdout.text())).toEqual(result.toJSON()); }); - test("keeps scan progress scope and completion paths redacted", async () => { + test("preserves scan progress scope and completion paths", async () => { const stdout = capture(); const stderr = capture(); const result = fakeResult(); @@ -4503,10 +4403,12 @@ describe("CLI", () => { dependencies({ result }), ), ).toBe(0); - expect(stderr.text()).not.toContain("SYNTHETIC_SCOPE_KEY_123"); - expect(stderr.text()).not.toContain("SYNTHETIC_OUTPUT_KEY_123"); - expect(stderr.text()).toContain("src/[redacted]"); - expect(stderr.text()).toContain("/tmp/scan_[redacted]"); + expect(stderr.text()).toContain("SYNTHETIC_SCOPE_KEY_123"); + expect(stderr.text()).toContain("SYNTHETIC_OUTPUT_KEY_123"); + expect(stderr.text()).toContain("src/sk-proj-SYNTHETIC_SCOPE_KEY_123"); + expect(stderr.text()).toContain( + "/tmp/scan_sk-proj-SYNTHETIC_OUTPUT_KEY_123", + ); }); test("reports parent fallback when delegated workers cannot start", async () => { @@ -4649,7 +4551,7 @@ describe("CLI", () => { expect(stderr.text()).not.toContain("Running scan"); }); - test("keeps redacted archive notices on stderr for JSON scans", async () => { + test("keeps archive notices on stderr for JSON scans", async () => { const stdout = capture(); const stderr = capture(); expect( @@ -4682,9 +4584,9 @@ describe("CLI", () => { expect(JSON.parse(stdout.text())).toEqual(fakeResult().toJSON()); expect(stderr.text()).toContain( "[00:00] Preparing scan\n" + - "Moved existing results to: /tmp/[redacted]/results.previous-20260721T031422-1234abcd\n", + "Moved existing results to: /tmp/sk-proj-SYNTHETIC_ARCHIVE_KEY_123/results.previous-20260721T031422-1234abcd\n", ); - expect(stderr.text()).not.toContain("SYNTHETIC_ARCHIVE_KEY_123"); + expect(stderr.text()).toContain("SYNTHETIC_ARCHIVE_KEY_123"); }); test("reports findings by severity and applies the requested policy", async () => { @@ -4953,7 +4855,7 @@ describe("CLI", () => { expect(stderr.text()).not.toContain("codex-security:"); }); - test("redacts credentials embedded in protected-root diagnostics", async () => { + test("preserves protected-root diagnostics", async () => { const stdout = capture(); const stderr = capture(); const protectedRoot = @@ -4978,16 +4880,16 @@ describe("CLI", () => { ).toBe(2); expect(stdout.text()).toBe(""); expect(stderr.text()).toContain( - "Resolved path: /private/tmp/worktree_[redacted]/results_[redacted]", + "Resolved path: /private/tmp/worktree_sk-proj-SYNTHETIC_ROOT_KEY_123/results_sk-proj-SYNTHETIC_OUTPUT_KEY_123", ); expect(stderr.text()).toContain( - "Protected root: /private/tmp/worktree_[redacted]", + "Protected root: /private/tmp/worktree_sk-proj-SYNTHETIC_ROOT_KEY_123", ); - expect(stderr.text()).not.toContain("SYNTHETIC_ROOT_KEY"); - expect(stderr.text()).not.toContain("SYNTHETIC_OUTPUT_KEY"); + expect(stderr.text()).toContain("SYNTHETIC_ROOT_KEY"); + expect(stderr.text()).toContain("SYNTHETIC_OUTPUT_KEY"); }); - test("redacts credentials from caught scan and interruption failures", async () => { + test("preserves caught scan and interruption failures", async () => { for (const failure of [ new CodexSecurityError(`scan failed ${SYNTHETIC_CREDENTIALS}`), new ScanInterruptedError( @@ -5011,12 +4913,12 @@ describe("CLI", () => { ).toBe(2); expect(stdout.text()).toBe(""); expect(stderr.text()).toBe( - "[00:00] Preparing scan\n" + `scan failed ${REDACTED_CREDENTIALS}\n`, + "[00:00] Preparing scan\n" + `scan failed ${SYNTHETIC_CREDENTIALS}\n`, ); } }); - test("redacts embedded credentials from retained partial-output paths", async () => { + test("preserves retained partial-output paths", async () => { const path = "/private/tmp/scan_sk-proj-SYNTHETIC_PATH_KEY_123/results"; for (const [signal, expectedExit] of [ [null, 2], @@ -5044,9 +4946,9 @@ describe("CLI", () => { ).toBe(expectedExit); expect(stdout.text()).toBe(""); expect(stderr.text()).toContain( - "Partial output was kept at /private/tmp/scan_[redacted]/results.", + "Partial output was kept at /private/tmp/scan_sk-proj-SYNTHETIC_PATH_KEY_123/results.", ); - expect(stderr.text()).not.toContain("SYNTHETIC_PATH_KEY"); + expect(stderr.text()).toContain("SYNTHETIC_PATH_KEY"); } }, 30_000); diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts deleted file mode 100644 index a48ec914..00000000 --- a/sdk/typescript/tests-ts/errors.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { redactedErrorMessage } from "../src/errors.js"; - -describe("security error redaction", () => { - test("redacts standalone and truncated PEM private-key envelopes", () => { - expect( - redactedErrorMessage( - "provider failed: -----BEGIN PRIVATE KEY-----\nSYNTHETIC_PRIVATE_KEY\n-----END PRIVATE KEY----- safe=value", - ), - ).toBe("provider failed: [redacted] safe=value"); - expect( - redactedErrorMessage( - "provider failed: -----BEGIN RSA PRIVATE KEY-----\nSYNTHETIC_TRUNCATED_KEY", - ), - ).toBe("provider failed: [redacted]"); - expect( - redactedErrorMessage( - "provider failed: -----BEGIN PGP PRIVATE KEY BLOCK-----\nSYNTHETIC_PGP_PRIVATE_KEY\n-----END PGP PRIVATE KEY BLOCK----- safe=value", - ), - ).toBe("provider failed: [redacted] safe=value"); - }); - - test("redacts every parameter from structured authorization schemes", () => { - expect( - redactedErrorMessage( - 'Authorization: Digest username="example", response=SYNTHETIC_DIGEST_SECRET', - ), - ).toBe("Authorization: Digest [redacted]"); - expect( - redactedErrorMessage("auth=Custom response=SYNTHETIC_AUTH_SECRET"), - ).toBe("auth=Custom [redacted]"); - expect( - redactedErrorMessage( - "Authorization: Custom key=SYNTHETIC_AUTH_SECRET https://example.test/safe", - ), - ).toBe("Authorization: Custom [redacted] https://example.test/safe"); - expect( - redactedErrorMessage( - 'Authorization: Digest username="a\\"b", response="SYNTHETIC_DIGEST_SECRET"', - ), - ).toBe("Authorization: Digest [redacted]"); - expect( - redactedErrorMessage( - "Authorization: Digest username*=UTF-8''user, response=SYNTHETIC_DIGEST_SECRET", - ), - ).toBe("Authorization: Digest [redacted]"); - expect( - redactedErrorMessage( - 'client_authorization_value=Digest username="example", response="SYNTHETIC_DIGEST_SECRET"', - ), - ).toBe("client_authorization_value=Digest [redacted]"); - expect( - redactedErrorMessage( - "Authorization: 0-custom 0nonce=visible, !response=SYNTHETIC_DIGEST_SECRET", - ), - ).toBe("Authorization: 0-custom [redacted]"); - }); - - test("redacts encoded API-key names without consuming other parameters", () => { - for (const separator of ["%5F", "%2D"]) { - const value = `https://example.test/?api${separator}key%3DSYNTHETIC_API_KEY%26safe%3Dvisible`; - expect(redactedErrorMessage(value)).toBe( - `https://example.test/?api${separator}key%3D[redacted]%26safe%3Dvisible`, - ); - } - }); - - test("redacts plural credential assignments and quoted values", () => { - expect( - redactedErrorMessage( - "credentials=SYNTHETIC_CREDENTIAL clientCredentials=SYNTHETIC_CLIENT apiKeys=SYNTHETIC_KEYS", - ), - ).toBe( - "credentials=[redacted] clientCredentials=[redacted] apiKeys=[redacted]", - ); - expect( - redactedErrorMessage('{"credentials":"correct horse battery staple"}'), - ).toBe('{"credentials":"[redacted]"}'); - expect( - redactedErrorMessage( - "credentials=[alice:SYNTHETIC_ONE,bob:SYNTHETIC_TWO] safe=visible", - ), - ).toBe("credentials=[redacted] safe=visible"); - expect( - redactedErrorMessage( - '{"credentials":["SYNTHETIC_ONE","SYNTHETIC_TWO"],"safe":"visible"}', - ), - ).toBe('{"credentials":"[redacted]","safe":"visible"}'); - expect( - redactedErrorMessage( - 'credentials_value=["SYNTHETIC_ONE","SYNTHETIC_TWO"] safe=visible', - ), - ).toBe("credentials_value=[redacted] safe=visible"); - expect( - redactedErrorMessage( - '{"api_keys_data":[{"key":"SYNTHETIC_ONE"},{"key":"SYNTHETIC_TWO"}]}', - ), - ).toBe('{"api_keys_data":"[redacted]"}'); - expect( - redactedErrorMessage("password=prefix\\SYNTHETIC_SECRET safe=visible"), - ).toBe("password=[redacted] safe=visible"); - }); -}); diff --git a/sdk/typescript/tests-ts/multiscan.test.ts b/sdk/typescript/tests-ts/multiscan.test.ts index 3ef41a0b..ebd67fc1 100644 --- a/sdk/typescript/tests-ts/multiscan.test.ts +++ b/sdk/typescript/tests-ts/multiscan.test.ts @@ -1223,22 +1223,6 @@ describe("multiscan", () => { const source = await repository(paths.root, "retry"); const secret = "sk-proj-SYNTHETIC_MULTISCAN_SECRET_123"; const knowledgeBasePaths = ["architecture.md"]; - const proxyUrl = - "https://SYNTHETIC_USER:SYNTHETIC_MULTISCAN_PASSWORD@proxy.test/v1/responses"; - const queryUrl = - "https://proxy.test/v1/responses?api_key=SYNTHETIC_MULTISCAN_QUERY_123&safe=1"; - const shortAuthorization = "Bearer abc123"; - const suffixedSecret = "SYNTHETIC_SUFFIXED_CLIENT_SECRET_123"; - const suffixedToken = "SYNTHETIC_SUFFIXED_ACCESS_TOKEN_123"; - const suffixedQuery = "SYNTHETIC_SUFFIXED_QUERY_SECRET_123"; - const quotedSecret = "SYNTHETIC correct horse battery staple"; - const opaqueAuthorization = "SYNTHETIC opaque authorization secret"; - const npmAuthorization = "SYNTHETIC_NPM_AUTH_VALUE_123"; - const customAuthorization = "SYNTHETIC_CUSTOM_AUTHORIZATION_123"; - const suffixedAuthorization = "SYNTHETIC_SUFFIXED_AUTHORIZATION_123"; - const paddedAuthorization = "SYNTHETIC_PADDED_AUTHORIZATION_TOKEN=="; - const keyedAuthorization = "SYNTHETIC_KEYED_AUTHORIZATION_SECRET_123"; - const camelCaseSecret = "SYNTHETIC_CAMEL_CASE_CLIENT_SECRET_123"; await writeFile( paths.input, `id,repository,revision\nretry,${source.path},${source.revision}\n`, @@ -1251,11 +1235,7 @@ describe("multiscan", () => { client(async (_repository, scanOptions = {}) => { expect(scanOptions.knowledgeBasePaths).toEqual(knowledgeBasePaths); attempts += 1; - if (attempts === 1) { - throw new Error( - `temporary failure ${secret} ${shortAuthorization} client_secret_value=${suffixedSecret} access_token_value=${suffixedToken} ${JSON.stringify({ client_secret_value: quotedSecret })} authorization="${opaqueAuthorization}" _auth=${npmAuthorization} Authorization: ApiKey ${customAuthorization} client_authorization_value=ApiKey ${suffixedAuthorization} auth=ApiKey ${paddedAuthorization} Authorization: Custom key=${keyedAuthorization} clientSecretValue=${camelCaseSecret} sending request for url (${proxyUrl}) and ${queryUrl}&client_secret_value=${suffixedQuery}`, - ); - } + if (attempts === 1) throw new Error(`temporary failure ${secret}`); return await completedScan(scanOptions.outputDir!); }), { knowledgeBasePaths }, @@ -1268,23 +1248,7 @@ describe("multiscan", () => { { id: "retry", status: "failed", attempt: 1 }, { id: "retry", status: "completed", attempt: 2 }, ]); - const ledger = await readFile(summary.resultsPath, "utf8"); - expect(ledger).not.toContain(secret); - expect(ledger).not.toContain("SYNTHETIC_MULTISCAN_PASSWORD"); - expect(ledger).not.toContain("SYNTHETIC_MULTISCAN_QUERY_123"); - expect(ledger).not.toContain(suffixedSecret); - expect(ledger).not.toContain(suffixedToken); - expect(ledger).not.toContain(suffixedQuery); - expect(ledger).not.toContain(quotedSecret); - expect(ledger).not.toContain(opaqueAuthorization); - expect(ledger).not.toContain(npmAuthorization); - expect(ledger).not.toContain(customAuthorization); - expect(ledger).not.toContain(suffixedAuthorization); - expect(ledger).not.toContain(paddedAuthorization); - expect(ledger).not.toContain(keyedAuthorization); - expect(ledger).not.toContain(camelCaseSecret); - expect(ledger).not.toContain(shortAuthorization); - expect(ledger).toContain("https://[redacted]@proxy.test/v1/responses"); + expect(await readFile(summary.resultsPath, "utf8")).toContain(secret); }); test("resumes complete bundles, repairs missing output, and rejects manifest drift", async () => { diff --git a/sdk/typescript/tests-ts/runtime.test.ts b/sdk/typescript/tests-ts/runtime.test.ts index 65a26b24..665a377e 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -2790,7 +2790,7 @@ describe("runtime directories and plugin Python boundary", () => { ).toBe(false); }); - test("surfaces redacted Windows ACL subprocess failures", async () => { + test("surfaces Windows ACL subprocess failures", async () => { const root = await temporaryDirectory(); const home = join(root, "home"); await mkdir(home); @@ -2812,9 +2812,8 @@ describe("runtime directories and plugin Python boundary", () => { } catch (error) { expect(error).toBeInstanceOf(Error); expect((error as Error).message).toContain("core types"); - expect((error as Error).message).toContain("token=[redacted]"); - expect((error as Error).message).not.toContain( - "SYNTHETIC_WINDOWS_ACL_SECRET", + expect((error as Error).message).toContain( + "token=sk-proj-SYNTHETIC_WINDOWS_ACL_SECRET_123", ); expect((error as Error).cause).toBe(underlying); } diff --git a/sdk/typescript/tests-ts/scan-dashboard.test.ts b/sdk/typescript/tests-ts/scan-dashboard.test.ts index 19ec8772..a2071979 100644 --- a/sdk/typescript/tests-ts/scan-dashboard.test.ts +++ b/sdk/typescript/tests-ts/scan-dashboard.test.ts @@ -554,7 +554,7 @@ describe("live scan dashboard", () => { dashboard.stop(); }); - test("redacts external Markdown link targets and rejects unsafe links", () => { + test("preserves external Markdown link targets and rejects unsafe links", () => { const stderr = capture(true); const dashboard = new ScanDashboard( { ...stderr.stream, columns: 120, rows: 18 }, @@ -562,7 +562,6 @@ describe("live scan dashboard", () => { repository: "/code/juice-shop", color: false, clock: fakeClock(), - sanitize: (value) => value.replaceAll("secret-token", "[redacted]"), }, ); @@ -579,9 +578,8 @@ describe("live scan dashboard", () => { const frame = lastFrame(stderr); expect(frame).toContain("See report, unsafe, and control."); expect(stderr.text()).toContain( - "\u001B]8;;https://example.com/report?token=[redacted]\u0007report\u001B]8;;\u0007", + "\u001B]8;;https://example.com/report?token=secret-token\u0007report\u001B]8;;\u0007", ); - expect(stderr.text()).not.toContain("secret-token"); expect(stderr.text()).not.toContain("javascript:"); expect(stderr.text()).not.toContain("spoof"); expect(stderr.text()).not.toContain("\u001B]8;;javascript:");