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

Filter by extension

Filter by extension

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

Expand Down
3 changes: 3 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. |
Expand Down Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
OutputDirectoryError,
OutputInsideProtectedRootError,
type ProtectedScanPathKind,
redactedErrorMessage,
errorMessage,
ScanCostLimitExceededError,
ScanInterruptedError,
} from "./errors.js";
Expand Down Expand Up @@ -1083,10 +1083,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)]
: []),
Expand All @@ -1100,12 +1098,12 @@ export class CodexSecurity {
throw new CodexSecurityError(turnFailureMessage(event["error"]));
}
}
} catch (postScanError) {
} catch {
notifyObserver(
"onWarning",
options.onWarning,
options.onObserverError,
`Could not run post-scan instructions: ${redactedErrorMessage(postScanError)}`,
"Could not run post-scan instructions.",
);
}
}
Expand Down
49 changes: 23 additions & 26 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import {
OutputDirectoryError,
OutputInsideProtectedRootError,
PluginPythonUnavailableError,
redactedErrorMessage,
errorMessage,
ScanCostLimitExceededError,
ScanInterruptedError,
} from "./errors.js";
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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([])
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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`,
);
},
});
Expand All @@ -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);
Expand Down Expand Up @@ -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`);
}
},
})
Expand Down Expand Up @@ -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`);
}
},
})
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -2651,15 +2651,15 @@ 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)
return errorMessage(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",
Expand Down Expand Up @@ -2876,7 +2876,6 @@ async function runScan(
: { maxCostUsd: arguments_.maxCostUsd }),
clock: dependencies,
color: dependencies.environment["NO_COLOR"] === undefined,
sanitize: redactedErrorMessage,
input: process.stdin,
onInterrupt,
});
Expand Down Expand Up @@ -2976,13 +2975,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 @@ -3207,7 +3206,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 @@ -3226,7 +3225,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 @@ -3409,9 +3408,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 @@ -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`,
Expand All @@ -3493,7 +3490,7 @@ function printScanSummary(
);
}
errorOutput.write(
` ${paint("RESULTS", 1)} ${redactedErrorMessage(result.scanDir)}\n`,
` ${paint("RESULTS", 1)} ${errorMessage(result.scanDir)}\n`,
);
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
},
);
Expand Down
69 changes: 2 additions & 67 deletions sdk/typescript/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,72 +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-----[\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]",
);
}

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);
export function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

/** Base error for Codex Security SDK failures. */
Expand Down
4 changes: 2 additions & 2 deletions sdk/typescript/src/multiscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 });
}
Expand Down
Loading
Loading