diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 83dc3be8..0b76455d 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,11 +2,59 @@ set -eu -if [ "${1:-}" = bulk-scan ]; then - case "${2:-}" in - --help|-h) +bulk_scan_command= +bulk_scan_input= +bulk_scan_metadata= +expects_option_value= + +for argument do + case "$argument" in + --help|-h|--llms|--llms-full|--schema|--version) + bulk_scan_metadata=yes + continue + ;; + esac + + if [ "$expects_option_value" = yes ]; then + if [ "$argument" = -- ] && [ "$bulk_scan_command" = yes ]; then + printf '%s\n' 'codex-security: bulk-scan does not support the -- option terminator.' >&2 + exit 2 + fi + expects_option_value= + continue + fi + + case "$argument" in + --) + if [ "$bulk_scan_command" = yes ]; then + printf '%s\n' 'codex-security: bulk-scan does not support the -- option terminator.' >&2 + exit 2 + fi + break + ;; + bulk-scan) + bulk_scan_command=yes ;; - ""|-*) + --output-dir|--workers|--mode|--model|--effort|--provider|\ + --knowledge-base|--max-attempts|--plugin-path|--python|\ + --codex|--filter-output|--format|\ + --token-limit|--token-offset) + expects_option_value=yes + ;; + -*) + ;; + *) + if [ "$bulk_scan_command" != yes ]; then + break + fi + bulk_scan_input=$argument + ;; + esac +done + +if [ "$bulk_scan_command" = yes ] && [ "$bulk_scan_metadata" != yes ]; then + case "$bulk_scan_input" in + "") printf '%s\n' 'codex-security: bulk-scan requires a repository CSV; interactive discovery is not supported in this image.' >&2 exit 2 ;; diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 9763b1f1..426133be 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -207,6 +207,7 @@ npx @openai/codex-security scan /path/to/repository --mode deep --workers 2 --su npx @openai/codex-security install-hook npx @openai/codex-security bulk-scan npx @openai/codex-security bulk-scan --model gpt-5.6-terra --effort high +npx @openai/codex-security bulk-scan --workers 4 --mode deep --max-attempts 3 npx @openai/codex-security bulk-scan repositories.csv --output-dir /path/outside/repositories/security-scans --workers 4 --knowledge-base /path/to/threat-models --knowledge-base /path/to/architecture.pdf npx @openai/codex-security scans list /path/to/repository npx @openai/codex-security scans list --scan-root /path/outside/repository/results @@ -466,6 +467,11 @@ Private checkouts reuse your GitHub CLI sign-in without changing your global Git configuration. The selected repositories are saved to `/repositories.csv` for review or resumption. +Interactive discovery accepts the same `--workers`, `--mode`, `--max-attempts`, +`--model`, `--effort`, `--plugin-path`, `--python`, and `--codex` settings as +CSV-driven scans. It prompts for the output directory; `--output-dir` is only +valid when a repository CSV is supplied. + To use an existing repository list or run in CI, pass a CSV with required `id`, `repository`, and `revision` columns. Revisions must be full commit hashes; optional `scope` and `mode` columns narrow individual scans: @@ -615,6 +621,10 @@ device login remains in `state/`. For unattended scans, set `OPENAI_API_KEY` or `CODEX_API_KEY` instead. Set `GH_TOKEN` or `GITHUB_TOKEN` for private GitHub repositories. +The container accepts the repository CSV before or after bulk-scan options. +Interactive repository discovery remains disabled, including when global CLI +options appear before `bulk-scan`. + On Ubuntu hosts that restrict unprivileged user namespaces, an administrator can install the optional, narrowly scoped AppArmor profile once: diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index bf905ad9..882cd0c1 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -1381,32 +1381,9 @@ export async function main( let outputDir: string; let githubHost: string | undefined; if (args.input === undefined) { - let optionIndex = 1; - while (optionIndex < argv.length) { - const argument = argv[optionIndex]!; - if ( - argument === "--model" || - argument === "--effort" || - argument === "--provider" || - argument === "--codex" || - argument === "--knowledge-base" - ) { - optionIndex += 2; - } else if ( - argument.startsWith("--model=") || - argument.startsWith("--effort=") || - argument.startsWith("--provider=") || - argument.startsWith("--codex=") || - argument.startsWith("--knowledge-base=") - ) { - optionIndex += 1; - } else { - break; - } - } - if (argv[0] !== "bulk-scan" || optionIndex !== argv.length) { + if (options.outputDir !== undefined) { throw new Error( - "Run 'codex-security bulk-scan [--provider PROVIDER] [--model MODEL] [--effort EFFORT] [--codex KEY=VALUE] [--knowledge-base PATH]' to discover repositories, or provide a CSV and --output-dir.", + "--output-dir can only be used with a repository CSV; omit it to choose an output directory interactively.", ); } const wizard = await runBulkScanWizard( diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 725e8b42..c7200d17 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -885,6 +885,10 @@ describe("CLI", () => { ["bulk-scan", "--codex", 'model_reasoning_effort="high"'], ["bulk-scan", '--codex=model_reasoning_effort="high"'], ["bulk-scan", "--model", "gpt-5.6-terra", "--effort", "high"], + ["bulk-scan", "--workers", "8", "--mode", "deep"], + ["bulk-scan", "--max-attempts=3", "--plugin-path", "./plugin"], + ["bulk-scan", "--python=python3"], + ["--format", "toon", "bulk-scan", "--workers", "8"], ["bulk-scan", "--knowledge-base", "/shared/threat-models"], [ "bulk-scan", @@ -941,6 +945,21 @@ describe("CLI", () => { expect(stdout.text()).toBe(""); }); + test("rejects an output directory without a repository CSV", async () => { + const stderr = capture(); + expect( + await main( + ["bulk-scan", "--output-dir", "results"], + capture().stream, + stderr.stream, + dependencies(), + ), + ).toBe(2); + expect(stderr.text()).toContain( + "--output-dir can only be used with a repository CSV", + ); + }); + test("exposes only typed, read-only SDK metadata over MCP", () => { const child = spawnSync( process.execPath, diff --git a/sdk/typescript/tests-ts/container-entrypoint.test.ts b/sdk/typescript/tests-ts/container-entrypoint.test.ts index 990655fe..ea3b9a90 100644 --- a/sdk/typescript/tests-ts/container-entrypoint.test.ts +++ b/sdk/typescript/tests-ts/container-entrypoint.test.ts @@ -166,6 +166,33 @@ describe("customer container entrypoint", () => { }, ); + testPosix("accepts CSVs after global and bulk-scan options", async () => { + for (const arguments_ of [ + ["bulk-scan", "--workers", "2", "/input/repositories.csv"], + [ + "--format", + "toon", + "bulk-scan", + "--output-dir=/output", + "/input/repositories.csv", + ], + ] as const) { + const result = await runEntrypoint(arguments_); + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe( + [ + ...arguments_, + ...(appArmorRestrictsUserNamespaces && + !usesCodexSecurityAppArmorProfile + ? ["--codex", "features.use_legacy_landlock=true"] + : []), + "", + ].join("\n"), + ); + } + }); + testPosix( "preserves bulk-scan help without injecting scan configuration", async () => { @@ -229,15 +256,33 @@ describe("customer container entrypoint", () => { testPosix( "rejects interactive discovery before starting the CLI", async () => { - const result = await runEntrypoint(["bulk-scan"]); + for (const arguments_ of [ + ["bulk-scan"], + ["bulk-scan", "--workers", "8"], + ["--format", "json", "bulk-scan", "--mode", "deep"], + ] as const) { + const result = await runEntrypoint(arguments_); + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain( + "interactive discovery is not supported", + ); + } + }, + ); + testPosix("rejects unsupported bulk-scan option terminators", async () => { + for (const arguments_ of [ + ["bulk-scan", "--output-dir", "/output", "--", "--help"], + ["bulk-scan", "--output-dir", "--", "--help"], + ] as const) { + const result = await runEntrypoint(arguments_); expect(result.status).toBe(2); - expect(result.stdout).toBe(""); - expect(result.stderr).toBe( - "codex-security: bulk-scan requires a repository CSV; interactive discovery is not supported in this image.\n", + expect(result.stderr).toContain( + "does not support the -- option terminator", ); - }, - ); + } + }); testPosix("does not change non-scan commands", async () => { const result = await runEntrypoint(["--version"]);