diff --git a/README.md b/README.md index 3b39ddfc..26c30741 100644 --- a/README.md +++ b/README.md @@ -74,15 +74,18 @@ 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. + +Verbose diagnostics may contain sensitive data. Review local logs before +sharing them. Saved failure summaries, bulk-scan receipts, and the interactive +dashboard omit messages that contain recognizable credentials. ## TypeScript SDK diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 1fe6764e..46bc2c17 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -400,7 +400,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 verbose 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`. | @@ -451,11 +451,12 @@ 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 +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. Verbose diagnostics may contain +sensitive data; review local logs before sharing them. The interactive +dashboard omits activity containing recognizable credentials. Each scan records its model, tokens, and estimated cost in its JSON result, scan history, and bulk-scan receipt. Estimates use @@ -514,7 +515,8 @@ least eight characters. Scan history uses the existing Codex Security workbench database at `$CODEX_HOME/state/plugins/codex-security/workbench.sqlite3`. Set `CODEX_SECURITY_STATE_DIR` to place the database elsewhere. Scan credentials -are never stored in the scan configuration. +are never stored in the scan configuration. Recorded failure summaries and +bulk-scan receipts omit messages that contain recognizable credentials. The scan sandbox permits writes to the selected state directory so SQLite can maintain its database and journal files. If the host itself cannot write to the diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index cb90e089..102363c6 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -48,7 +48,8 @@ import { OutputDirectoryError, OutputInsideProtectedRootError, type ProtectedScanPathKind, - redactedErrorMessage, + errorMessage, + safeErrorMessage, ScanCostLimitExceededError, ScanInterruptedError, } from "./errors.js"; @@ -684,7 +685,7 @@ export class CodexSecurity { "onWarning", options.onWarning, options.onObserverError, - `Could not track scan activity: ${redactedErrorMessage(error)}`, + `Could not track scan activity: ${errorMessage(error)}`, ); }; const tracker = new ScanCostTracker({ @@ -1108,10 +1109,9 @@ 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. + // Scan history can be shared; never persist credential-bearing failures. "--message", - redactedErrorMessage(failure).slice(0, 2400), + safeErrorMessage(failure).slice(0, 2400), ...(snapshot?.cost ? ["--cost-json", JSON.stringify(snapshot.cost)] : []), @@ -1130,7 +1130,7 @@ export class CodexSecurity { "onWarning", options.onWarning, options.onObserverError, - `Could not run post-scan instructions: ${redactedErrorMessage(postScanError)}`, + `Could not run post-scan instructions: ${errorMessage(postScanError)}`, ); } } @@ -1705,7 +1705,7 @@ export async function initialCredentialsAvailable( // Reports a cleanup failure without letting it decide the result of the scan. Only the // message is forwarded, and it reaches the onWarning observer alone: unlike the fail-scan -// path it is never written to the workbench, so it adds no persisted, unredacted text. +// path it is never written to the workbench, so it adds no persisted warning text. function warnCleanupFailed( options: Pick, reason: unknown, @@ -2402,8 +2402,8 @@ function reconnectDetails(message: string): ScanReconnectDetails | undefined { // // Only `error.message` is reused, because that is the single shape the previous // code already surfaced. No other shape is forwarded or stringified: this message -// reaches `fail-scan --message` and is stored in `scans.failure_message` without -// redaction, so widening what is copied out of the payload would add a new +// reaches `fail-scan --message` and is stored unchanged in `scans.failure_message`, +// so widening what is copied out of the payload would add a new // credential-disclosure path to persistent scan history. function turnFailureMessage(error: unknown): string { if (isRecord(error) && typeof error["message"] === "string") { diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 1a2a3f4a..843837ff 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -66,7 +66,8 @@ import { OutputDirectoryError, OutputInsideProtectedRootError, PluginPythonUnavailableError, - redactedErrorMessage, + errorMessage, + safeErrorMessage, ScanCostLimitExceededError, ScanInterruptedError, } from "./errors.js"; @@ -715,7 +716,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; } @@ -921,7 +922,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({ @@ -984,7 +985,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; } @@ -1037,7 +1038,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([]) @@ -1298,7 +1299,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; } @@ -1460,7 +1461,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`, ); }, }); @@ -1474,7 +1475,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); @@ -1578,7 +1579,7 @@ export async function main( ); } catch (error) { exitCode = 2; - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); } }, }) @@ -1614,7 +1615,7 @@ export async function main( ); } catch (error) { exitCode = 2; - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); } }, }) @@ -1803,7 +1804,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; } @@ -1812,7 +1813,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; } } @@ -2533,20 +2534,18 @@ async function runExport( } return 0; } catch (error) { - errorOutput.write(`codex-security: ${redactedErrorMessage(error)}\n`); + errorOutput.write(`codex-security: ${errorMessage(error)}\n`); return 2; } } type VerboseDiagnosticValue = string | number | boolean | null | undefined; -function sanitizeDiagnosticValue(value: unknown): string { - return redactedErrorMessage(value) - .replaceAll( - /(\b(?:tenant(?:[_-]?id)?|org(?:anization)?(?:[_-]?id)?|project(?:[_-]?id)?|(?:x[_-]?)?(?:request|trace|correlation)[_-]?id)\b(?:\\*["'])?\s*[:=]\s*)(?!\[redacted\])(?:(\\*)(['"])(?:(?!(? { @@ -2758,7 +2757,7 @@ async function runScan( : { maxCostUsd: arguments_.maxCostUsd }), clock: dependencies, color: dependencies.environment["NO_COLOR"] === undefined, - sanitize: redactedErrorMessage, + sanitize: safeErrorMessage, input: process.stdin, onInterrupt, }); @@ -2858,13 +2857,13 @@ async function runScan( diagnostic("scan.output_archived", { archive_dir: archiveDir }); if (dashboard !== null) { dashboard.note( - `Moved existing results to: ${redactedErrorMessage(archiveDir)}`, + `Moved existing results to: ${errorMessage(archiveDir)}`, ); return; } progress?.stopTimer(); errorOutput.write( - `Moved existing results to: ${redactedErrorMessage(archiveDir)}\n`, + `Moved existing results to: ${errorMessage(archiveDir)}\n`, ); }, signal: preparationAbortController.signal, @@ -3017,7 +3016,7 @@ async function runScan( progress.startTimer(runningMessage()); }, onWarning: (warning, details) => { - const message = sanitizeDiagnosticValue(warning); + const message = diagnosticValue(warning); if (details?.kind === "target_changed") { targetWarnings.push(message); } @@ -3031,7 +3030,7 @@ async function runScan( observer, classification: classifyConnectionFailure(error), }); - const warning = `${observer} observer failed: ${sanitizeDiagnosticValue(error)}`; + const warning = `${observer} observer failed: ${diagnosticValue(error)}`; if (dashboard === null) { writeAboveProgress(() => { errorOutput.write(`codex-security: warning: ${warning}\n`); @@ -3089,7 +3088,7 @@ async function runScan( failure instanceof ScanCostLimitExceededError ? failure : undefined; const message = failure instanceof OutputInsideProtectedRootError - ? redactedErrorMessage(protectedRootErrorMessage(failure)) + ? errorMessage(protectedRootErrorMessage(failure)) : scanFailureMessage(failure, selectedAuthentication); diagnostic("scan.failed", { classification: @@ -3108,7 +3107,7 @@ async function runScan( } if (scanDir !== null) { errorOutput.write( - `Partial output was kept at ${redactedErrorMessage(scanDir)}.\n`, + `Partial output was kept at ${errorMessage(scanDir)}.\n`, ); } return { exitCode: 2, error: message }; @@ -3245,7 +3244,7 @@ function scanFailureMessage( // appending it. That is deliberate: upstream authentication and authorization // errors can name the organization or project, which must not reach stderr or // the JSON error field. - if (isLocalScanFailure(error)) return sanitizeDiagnosticValue(error); + if (isLocalScanFailure(error)) return diagnosticValue(error); switch (classifyConnectionFailure(error)) { case "unauthorized": if (authentication?.method === "aws_credentials") { @@ -3277,7 +3276,7 @@ function scanFailureMessage( case "network_error": case "timeout": case "unknown": - return sanitizeDiagnosticValue(error); + return diagnosticValue(error); } } @@ -3291,9 +3290,7 @@ function scanScope(arguments_: ScanArguments): string | null { portable.startsWith("//") ? portable.split("/").at(-1) ?? portable : portable; - return redactedErrorMessage( - scoped.replaceAll(/[\u0000-\u001F\u007F]/gu, " "), - ); + return errorMessage(scoped.replaceAll(/[\u0000-\u001F\u007F]/gu, " ")); }); return `${displayed.join(", ")}${arguments_.paths.length > displayed.length ? `, +${arguments_.paths.length - displayed.length} more` : ""}`; } @@ -3359,7 +3356,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`, @@ -3375,7 +3372,7 @@ function printScanSummary( ); } errorOutput.write( - ` ${paint("RESULTS", 1)} ${redactedErrorMessage(result.scanDir)}\n`, + ` ${paint("RESULTS", 1)} ${errorMessage(result.scanDir)}\n`, ); } @@ -3710,7 +3707,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; } @@ -3736,7 +3733,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 a8face06..53d67c7f 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -1,72 +1,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]", - ); - return redactQuotedCredentialValues(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|\+)+)(?!\[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, - "$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[_-]?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, - "$1[redacted]", - ); +/** Returns the original error message without altering its contents. */ +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); } -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; - 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); +/** Omit credential-bearing messages at persistence and display boundaries. */ +export function safeErrorMessage(error: unknown): string { + const message = errorMessage(error); + const recognizableCredential = + /(?:\b(?:sk-(?:proj-)?|github_pat_|gh[pousr]_|npm_)\S+|\b(?:bearer|basic|token)(?:\s|%20|\+)+\S+|:\/\/[^\s/@]+@|-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----)/iu.test( + message, + ); + const assignments = message.matchAll( + /(? + /(?:api(?:[_-]|%5f|%2d)?key|access(?:[_-]|%5f|%2d)?key|private(?:[_-]|%5f|%2d)?key|authorization|auth(?!or)|token|secret|credential|signature|sig(?=[^A-Za-z0-9]|value|data|token|secret|credential|password|header|field|id|key|$)|password|passwd)/iu.test( + field, + ), + ); + return recognizableCredential || sensitiveField ? "[redacted]" : message; } /** Base error for Codex Security SDK failures. */ diff --git a/sdk/typescript/src/multiscan.ts b/sdk/typescript/src/multiscan.ts index e90e78ea..f3b962ab 100644 --- a/sdk/typescript/src/multiscan.ts +++ b/sdk/typescript/src/multiscan.ts @@ -20,7 +20,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 { safeErrorMessage } from "./errors.js"; import type { CoverageDocument } from "./models.js"; import type { ScanMode } from "./targets.js"; import { resolveTrustedExecutable } from "./trusted-executable.js"; @@ -247,7 +247,7 @@ async function runCampaign( } } catch (error) { if (options.signal?.aborted === true) options.signal.throwIfAborted(); - failure = redactedErrorMessage(error); + failure = safeErrorMessage(error); } finally { await rm(checkout, { recursive: true, force: true }); } diff --git a/sdk/typescript/src/runtime.ts b/sdk/typescript/src/runtime.ts index c57ffaba..9226cf47 100644 --- a/sdk/typescript/src/runtime.ts +++ b/sdk/typescript/src/runtime.ts @@ -43,7 +43,7 @@ import { OutputDirectoryError, PluginBootstrapError, PluginPythonUnavailableError, - redactedErrorMessage, + errorMessage, } from "./errors.js"; import type { JsonObject } from "./config.js"; import { resolveTrustedExecutable } from "./trusted-executable.js"; @@ -279,11 +279,11 @@ function windowsCredentialAclFailure(error: unknown): string { : error instanceof Error ? error.message : String(error); - const sanitized = redactedErrorMessage(detail) + const normalized = errorMessage(detail) .replace(/\s+/gu, " ") .trim() .slice(0, 512); - return sanitized === "" ? "" : `. ${sanitized}`; + return normalized === "" ? "" : `. ${normalized}`; } const WINDOWS_SYSTEM_SID = "S-1-5-18"; diff --git a/sdk/typescript/src/scan-dashboard.ts b/sdk/typescript/src/scan-dashboard.ts index 26713d07..06b1a3b2 100644 --- a/sdk/typescript/src/scan-dashboard.ts +++ b/sdk/typescript/src/scan-dashboard.ts @@ -361,6 +361,7 @@ export class ScanDashboard { value: string, kind: DashboardActivityLine["kind"], ): void => { + value = this.#options.sanitize?.(value) ?? value; if (kind !== "message" && kind !== "reasoning") { for (const text of wrapActivity(prefix, value, width)) { lines.push({ text, kind }); diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index ec8076ae..664a77e8 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -51,7 +51,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< @@ -3303,7 +3303,7 @@ describe("CodexSecurity orchestration", () => { await client.close(); }); - test("redacts credentials from the stored scan failure message", async () => { + test("keeps credential-bearing failures out of saved scan history", async () => { const root = await temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -3322,7 +3322,8 @@ describe("CodexSecurity orchestration", () => { const quotedCredential = JSON.stringify({ client_secret_value: "SYNTHETIC correct horse battery staple", }); - const redactedFailure = `${REDACTED_CREDENTIALS} {"client_secret_value":"[redacted]"}`; + const originalFailure = `${SYNTHETIC_CREDENTIALS} ${quotedCredential}`; + const storedFailure = "[redacted]"; const client = new TestClient( {}, { @@ -3348,7 +3349,7 @@ describe("CodexSecurity orchestration", () => { async function* failingEvents(): AsyncGenerator { yield { type: "error", - message: `${SYNTHETIC_CREDENTIALS} ${quotedCredential}`, + message: originalFailure, }; } return { events: failingEvents() }; @@ -3358,14 +3359,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(storedFailure); // `scans show` reads the stored message back through get-scan. const context = await runWorkbench( @@ -3374,11 +3373,9 @@ describe("CodexSecurity orchestration", () => { ); expect(context["scan"]).toMatchObject({ progress: { status: "failed" }, - failureMessage: redactedFailure, + failureMessage: storedFailure, }); - // 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"); 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..4b0335e1 100644 --- a/sdk/typescript/tests-ts/cli-fixtures.ts +++ b/sdk/typescript/tests-ts/cli-fixtures.ts @@ -73,58 +73,6 @@ export const SYNTHETIC_CREDENTIALS = [ "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 function capture(isTTY = false): { stream: Pick & Partial>; 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..c6d49f1a 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("preserves workbench failures and does not initialize Codex", async () => { const stderr = capture(); let started = false; expect( @@ -637,8 +632,8 @@ 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(stderr.text()).toContain("SYNTHETIC_KEY_123"); expect(started).toBe(false); }); }); diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index b76522e5..a1b3642b 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("preserves false-positive workbench failures", async () => { const stdout = capture(); const stderr = capture(); let started = false; @@ -529,8 +527,8 @@ 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(stderr.text()).toContain("SYNTHETIC_KEY_123"); expect(started).toBe(false); }); @@ -837,7 +835,7 @@ describe("CLI", () => { }, ); - test("preserves the bulk-scan failure summary and redacts progress errors", async () => { + test("keeps credentials out of bulk-scan failures and progress", async () => { const root = await mkdtemp(join(tmpdir(), "codex-security-cli-multiscan-")); try { await multiscanInventory(root); @@ -1707,7 +1705,7 @@ describe("CLI", () => { ); expect(text).not.toContain("thinking ·"); expect(text).not.toContain("said ·"); - expect(text).toContain('curl -H "Authorization: Bearer [redacted]"'); + expect(text).toContain("[redacted]"); expect(text).not.toContain("SYNTHETIC_OPENAI_VALUE_123"); expect(text).not.toContain("Building the file inventory"); expect(text).not.toContain("Running a scan command"); @@ -2246,7 +2244,7 @@ describe("CLI", () => { } }); - test("redacts malformed --codex overrides and accepts large values", () => { + test("does not echo malformed --codex overrides and accepts large values", () => { const secret = "SYNTHETIC_TOML_SECRET_MUST_NOT_ECHO"; let malformed: unknown; try { @@ -3094,7 +3092,7 @@ describe("CLI", () => { } }); - test("redacts verbose provider failures and excludes private provider context", async () => { + test("classifies provider failures without including upstream context", async () => { const stdout = capture(); const stderr = capture(); const deps = dependencies({ @@ -3127,7 +3125,7 @@ describe("CLI", () => { expect(stderr.text()).not.toContain("SYNTHETIC_PROVIDER_SECRET"); }); - test("excludes unclassified provider context from verbose failure diagnostics", async () => { + test("keeps unclassified provider context out of structured failure diagnostics", async () => { const stdout = capture(); const stderr = capture(); const deps = dependencies(); @@ -3161,12 +3159,12 @@ describe("CLI", () => { expect(failureDiagnostic).not.toContain("tenant-private"); expect(failureDiagnostic).not.toContain("req-internal"); expect(stderr.text()).toContain("Provider failed for"); - expect(stderr.text()).not.toContain("tenant-private"); - expect(stderr.text()).not.toContain("req-internal"); + expect(stderr.text()).toContain("tenant-private"); + expect(stderr.text()).toContain("req-internal"); expect(stdout.text()).toBe(""); }); - test("redacts provider identifier variants in scan failures", async () => { + test("preserves provider identifier variants in scan failures", async () => { const cases = [ { message: @@ -3258,15 +3256,14 @@ describe("CLI", () => { ).toBe(2); expect(stdout.text()).toBe(""); expect(stderr.text()).toContain("Provider failed for"); - expect(stderr.text()).toContain("[redacted]"); for (const identifier of identifiers) { - expect(stderr.text()).not.toContain(identifier); + expect(stderr.text()).toContain(identifier); } } } }); - test("redacts provider identifiers from scanner warnings", async () => { + test("preserves provider identifiers in scanner warnings", async () => { for (const verbose of [false, true]) { const stdout = capture(); const stderr = capture(); @@ -3302,10 +3299,9 @@ describe("CLI", () => { expect(stderr.text()).toContain( "codex-security: warning: Provider warning", ); - expect(stderr.text()).toContain("[redacted]"); - expect(stderr.text()).not.toContain("organization private"); - expect(stderr.text()).not.toContain("request private"); - expect(stderr.text()).not.toContain("tenant-private"); + expect(stderr.text()).toContain("organization private"); + expect(stderr.text()).toContain("request private"); + expect(stderr.text()).toContain("tenant-private"); } }); @@ -3356,7 +3352,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(); @@ -3388,16 +3384,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"); }); test("excludes observer failure context from verbose diagnostics", async () => { @@ -3441,8 +3436,8 @@ describe("CLI", () => { expect(observerDiagnostic).not.toContain("tenant-private"); expect(observerDiagnostic).not.toContain("req-internal"); expect(stderr.text()).toContain("Observer failed for"); - expect(stderr.text()).not.toContain("tenant-private"); - expect(stderr.text()).not.toContain("req-internal"); + expect(stderr.text()).toContain("tenant-private"); + expect(stderr.text()).toContain("req-internal"); }); test("excludes cleanup failure context from verbose diagnostics", async () => { @@ -3477,8 +3472,8 @@ describe("CLI", () => { } expect(stderr.text()).toContain("Cleanup failed for"); - expect(stderr.text()).not.toContain("tenant-private"); - expect(stderr.text()).not.toContain("req-internal"); + expect(stderr.text()).toContain("tenant-private"); + expect(stderr.text()).toContain("req-internal"); expect(stdout.text()).toBe(""); }); @@ -3714,7 +3709,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(); @@ -3733,106 +3728,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(); @@ -3857,7 +3758,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 () => { @@ -4042,7 +3943,7 @@ describe("CLI", () => { } }); - test("emits redacted scan warnings in verbose diagnostics", async () => { + test("preserves scan warnings in verbose diagnostics", async () => { const stdout = capture(); const stderr = capture(); const deps = dependencies(); @@ -4067,12 +3968,12 @@ 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"); + expect(stderr.text()).toContain("SYNTHETIC_WARNING_SECRET"); }); test("prints granted trusted cyber access without warning or corrupting JSON scans", async () => { @@ -4197,9 +4098,9 @@ 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"); + expect(stderr.text()).toContain("SYNTHETIC_OPENAI_VALUE_123"); }); test("maps failed scan stdout writes to the runtime-error exit code", async () => { @@ -4503,7 +4404,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(); @@ -4525,10 +4426,10 @@ 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("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 () => { @@ -4671,7 +4572,7 @@ describe("CLI", () => { expect(stderr.text()).not.toContain("Running scan"); }); - test("keeps redacted archive notices on stderr for JSON scans", async () => { + test("keeps original archive notices on stderr for JSON scans", async () => { const stdout = capture(); const stderr = capture(); expect( @@ -4704,9 +4605,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 () => { @@ -4975,7 +4876,7 @@ describe("CLI", () => { expect(stderr.text()).not.toContain("codex-security:"); }); - test("redacts credentials embedded in protected-root diagnostics", async () => { + test("preserves complete protected-root diagnostics", async () => { const stdout = capture(); const stderr = capture(); const protectedRoot = @@ -4999,17 +4900,11 @@ describe("CLI", () => { ), ).toBe(2); expect(stdout.text()).toBe(""); - expect(stderr.text()).toContain( - "Resolved path: /private/tmp/worktree_[redacted]/results_[redacted]", - ); - expect(stderr.text()).toContain( - "Protected root: /private/tmp/worktree_[redacted]", - ); - expect(stderr.text()).not.toContain("SYNTHETIC_ROOT_KEY"); - expect(stderr.text()).not.toContain("SYNTHETIC_OUTPUT_KEY"); + expect(stderr.text()).toContain(`Resolved path: ${output}`); + expect(stderr.text()).toContain(`Protected root: ${protectedRoot}`); }); - 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( @@ -5033,12 +4928,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], @@ -5065,10 +4960,7 @@ describe("CLI", () => { await main(["scan", "."], stdout.stream, stderr.stream, deps), ).toBe(expectedExit); expect(stdout.text()).toBe(""); - expect(stderr.text()).toContain( - "Partial output was kept at /private/tmp/scan_[redacted]/results.", - ); - expect(stderr.text()).not.toContain("SYNTHETIC_PATH_KEY"); + expect(stderr.text()).toContain(`Partial output was kept at ${path}.`); } }, 30_000); diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts new file mode 100644 index 00000000..35be8c96 --- /dev/null +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "bun:test"; +import { errorMessage, safeErrorMessage } from "../src/errors.js"; + +describe("error messages", () => { + test("preserves error messages exactly", () => { + const message = "request failed: token=SYNTHETIC_TOKEN"; + expect(errorMessage(new Error(message))).toBe(message); + expect(errorMessage(message)).toBe(message); + }); + + test("formats non-error values without parsing them", () => { + expect(errorMessage(42)).toBe("42"); + expect(errorMessage(null)).toBe("null"); + }); + + test("omits credential-bearing messages at output boundaries", () => { + for (const message of [ + "request failed: token=SYNTHETIC_TOKEN", + "Authorization: Bearer sk-proj-SYNTHETIC_KEY_123", + 'upstream failed: {"clientSecret":"correct horse battery staple"}', + JSON.stringify(JSON.stringify({ clientSecret: "SYNTHETIC_SECRET" })), + "authorizationHeaderValue=SYNTHETIC_SECRET", + "api_key_header_value=SYNTHETIC_SECRET", + 'config["api_key"]="SYNTHETIC_SECRET"', + JSON.stringify('config["api_key"]="SYNTHETIC_SECRET"'), + encodeURIComponent(JSON.stringify({ api_key: "SYNTHETIC_SECRET" })), + "https://example.test/?credentials[access_token]=SYNTHETIC_SECRET", + "https://example.test/?user[password]=SYNTHETIC_SECRET", + "https://example.test/?config[api_key]=SYNTHETIC_SECRET", + "https://example.test/?access%5Fkey=SYNTHETIC_SECRET", + "https://example.test/?private%2Dkey=SYNTHETIC_SECRET", + "sig_value=SYNTHETIC_SIGNATURE", + "sigHeader=SYNTHETIC_SIGNATURE", + "proxy https://user:SYNTHETIC_PASSWORD@example.test", + "-----BEGIN PRIVATE KEY-----\nSYNTHETIC_PRIVATE_KEY", + "-----BEGIN PGP PRIVATE KEY BLOCK-----\nSYNTHETIC_PRIVATE_KEY", + ]) { + expect(safeErrorMessage(new Error(message))).toBe("[redacted]"); + } + expect(safeErrorMessage("upstream service unavailable")).toBe( + "upstream service unavailable", + ); + expect(safeErrorMessage("author=Michael")).toBe("author=Michael"); + expect(safeErrorMessage("signal=active")).toBe("signal=active"); + expect(safeErrorMessage("design=complete")).toBe("design=complete"); + expect(safeErrorMessage('worker 1: rg -n "password" src/login.ts')).toBe( + 'worker 1: rg -n "password" src/login.ts', + ); + expect(safeErrorMessage("secret".repeat(4_000))).toBe( + "secret".repeat(4_000), + ); + }); +}); diff --git a/sdk/typescript/tests-ts/multiscan.test.ts b/sdk/typescript/tests-ts/multiscan.test.ts index e0594f13..bfc3ba29 100644 --- a/sdk/typescript/tests-ts/multiscan.test.ts +++ b/sdk/typescript/tests-ts/multiscan.test.ts @@ -1320,22 +1320,8 @@ describe("multiscan", () => { { 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(ledger).toContain('"error":"[redacted]"'); + expect(ledger).not.toContain("SYNTHETIC"); }); 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 8176d95a..c9483a58 100644 --- a/sdk/typescript/tests-ts/runtime.test.ts +++ b/sdk/typescript/tests-ts/runtime.test.ts @@ -2718,7 +2718,7 @@ describe("runtime directories and plugin Python boundary", () => { ).toBe(false); }); - test("surfaces redacted Windows ACL subprocess failures", async () => { + test("preserves Windows ACL subprocess failures", async () => { const root = await temporaryDirectory(); const home = join(root, "home"); await mkdir(home); @@ -2740,9 +2740,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..048aa4a4 100644 --- a/sdk/typescript/tests-ts/scan-dashboard.test.ts +++ b/sdk/typescript/tests-ts/scan-dashboard.test.ts @@ -598,6 +598,34 @@ describe("live scan dashboard", () => { dashboard.stop(); }); + test("sanitizes complete activity descriptions before line wrapping", () => { + const stderr = capture(true); + const dashboard = new ScanDashboard( + { ...stderr.stream, columns: 50, rows: 18 }, + { + repository: "/code/juice-shop", + color: false, + clock: fakeClock(), + sanitize: (value) => + value.includes("client_secret=") ? "[redacted]" : value, + }, + ); + + dashboard.start(); + dashboard.record({ + id: "wrapped-secret", + kind: "command", + status: "completed", + description: + "Checking request configuration client_secret= SYNTHETIC_SECRET_VALUE", + paths: [], + }); + + expect(lastFrame(stderr)).toContain("[redacted]"); + expect(stderr.text()).not.toContain("SYNTHETIC_SECRET_VALUE"); + dashboard.stop(); + }); + test("colors important activity while keeping prose readable across terminal themes", () => { const stderr = capture(true); const dashboard = new ScanDashboard(stderr.stream, {