Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dd61f0d
fix(sdk): redact standalone keys and structured credentials
mldangelo-oai Aug 8, 2026
552c382
fix(sdk): preserve diagnostics after structured auth headers
mldangelo-oai Aug 8, 2026
e8b9589
fix(sdk): redact structured auth and credential collections
mldangelo-oai Aug 8, 2026
bf34227
fix(sdk): redact suffixed credential collections
mldangelo-oai Aug 8, 2026
c2d786b
fix(sdk): close credential redaction review edge cases
mldangelo-oai Aug 8, 2026
68b11bf
refactor(sdk): remove local credential redaction
mldangelo-oai Aug 10, 2026
a75ea4a
Merge main and preserve safe post-scan warnings
mldangelo-oai Aug 10, 2026
4de6057
fix(sdk): preserve redaction and cover standalone private keys
mldangelo-oai Aug 10, 2026
179b234
fix(sdk): match private-key boundaries before redaction
mldangelo-oai Aug 10, 2026
ae623dc
Match assigned private-key opening and closing labels
mldangelo-oai Aug 10, 2026
6577fa6
Simplify private-key redaction and preserve overlapping credentials
mldangelo-oai Aug 10, 2026
9051e66
Merge overlapping private-key and quoted-credential redactions
mldangelo-oai Aug 10, 2026
9fec366
fix(sdk): redact overlapping private-key blocks safely
mldangelo-oai Aug 10, 2026
be6012e
fix(sdk): redact nested private keys in a single pass
mldangelo-oai Aug 10, 2026
ef23ce1
fix(sdk): verify private-key delimiter boundaries
mldangelo-oai Aug 10, 2026
1461d02
fix(sdk): bind key delimiters to serialized line depth
mldangelo-oai Aug 10, 2026
9e2a6e5
fix(sdk): isolate credential and serialized key boundaries
mldangelo-oai Aug 10, 2026
a444919
fix(sdk): protect framed private keys and nearby auth values
mldangelo-oai Aug 10, 2026
de6293a
fix(sdk): preserve serialized private-key framing
mldangelo-oai Aug 10, 2026
124a84e
fix(sdk): retain valid serialized credential diagnostics
mldangelo-oai Aug 10, 2026
09a5e0b
merge: sync current repository guidance
mldangelo-oai Aug 10, 2026
a40e8c0
refactor(sdk): remove credential redaction
mldangelo-oai Aug 11, 2026
558138b
fix(sdk): keep credentials out of shared scan output
mldangelo-oai Aug 11, 2026
82c9e98
fix(sdk): sanitize dashboard events before wrapping
mldangelo-oai Aug 11, 2026
3d699b8
fix(sdk): recognize credential fields without unbounded backtracking
mldangelo-oai Aug 11, 2026
6120c67
fix(sdk): inspect complete credential assignments in linear time
mldangelo-oai Aug 11, 2026
b321ad2
refactor(sdk): simplify shared-error checks and preserve local diagno…
mldangelo-oai Aug 11, 2026
1370c28
fix(sdk): protect composite signature fields in shared errors
mldangelo-oai Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 7 additions & 5 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ import {
OutputDirectoryError,
OutputInsideProtectedRootError,
type ProtectedScanPathKind,
redactedErrorMessage,
errorMessage,
safeErrorMessage,
ScanCostLimitExceededError,
ScanInterruptedError,
} from "./errors.js";
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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)]
: []),
Expand All @@ -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)}`,
);
}
}
Expand Down Expand Up @@ -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<ScanOptions, "onWarning" | "onObserverError">,
reason: unknown,
Expand Down Expand Up @@ -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") {
Expand Down
71 changes: 34 additions & 37 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ import {
OutputDirectoryError,
OutputInsideProtectedRootError,
PluginPythonUnavailableError,
redactedErrorMessage,
errorMessage,
safeErrorMessage,
ScanCostLimitExceededError,
ScanInterruptedError,
} from "./errors.js";
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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([])
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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`,
);
},
});
Expand All @@ -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);
Expand Down Expand Up @@ -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`);
}
},
})
Expand Down Expand Up @@ -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`);
}
},
})
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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\])(?:(\\*)(['"])(?:(?!(?<!\\)\2\3)(?:\\.|[^\\]))*(?:(?<!\\)\2\3|$)|[^\s"',;&}\]]+)/giu,
"$1$2$3[redacted]$2$3",
)
.replaceAll(/[\u0000-\u001F\u007F\u0085\u2028\u2029]/gu, " ");
function diagnosticValue(value: unknown): string {
return errorMessage(value).replaceAll(
/[\u0000-\u001F\u007F\u0085\u2028\u2029]/gu,
" ",
);
}

async function runScan(
Expand Down Expand Up @@ -2589,7 +2588,7 @@ async function runScan(
value === undefined
? []
: [
`${name}=${JSON.stringify(typeof value === "string" ? sanitizeDiagnosticValue(value) : value)}`,
`${name}=${JSON.stringify(typeof value === "string" ? diagnosticValue(value) : value)}`,
],
);
writeAboveProgress(() => {
Expand Down Expand Up @@ -2758,7 +2757,7 @@ async function runScan(
: { maxCostUsd: arguments_.maxCostUsd }),
clock: dependencies,
color: dependencies.environment["NO_COLOR"] === undefined,
Comment thread
mldangelo-oai marked this conversation as resolved.
sanitize: redactedErrorMessage,
sanitize: safeErrorMessage,
Comment thread
mldangelo-oai marked this conversation as resolved.
input: process.stdin,
onInterrupt,
});
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand All @@ -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`);
Expand Down Expand Up @@ -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:
Expand All @@ -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 };
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -3277,7 +3276,7 @@ function scanFailureMessage(
case "network_error":
case "timeout":
case "unknown":
return sanitizeDiagnosticValue(error);
return diagnosticValue(error);
}
}

Expand All @@ -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` : ""}`;
}
Expand Down Expand Up @@ -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`,
Expand All @@ -3375,7 +3372,7 @@ function printScanSummary(
);
}
errorOutput.write(
` ${paint("RESULTS", 1)} ${redactedErrorMessage(result.scanDir)}\n`,
` ${paint("RESULTS", 1)} ${errorMessage(result.scanDir)}\n`,
);
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
},
);
Expand Down
Loading
Loading