Skip to content
Merged
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
58 changes: 54 additions & 4 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,61 @@

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
if [ "$argument" = -- ] && [ "$bulk_scan_command" = yes ]; then
printf '%s\n' 'codex-security: bulk-scan does not support the -- option terminator.' >&2
exit 2
fi

case "$argument" in
--help|-h|--llms|--llms-full|--schema|--version)
bulk_scan_metadata=yes
continue
Comment thread
mldangelo-oai marked this conversation as resolved.
;;
esac

if [ "$expects_option_value" = yes ]; then
expects_option_value=
continue
fi

case "$argument" in
--)
Comment thread
mldangelo-oai marked this conversation as resolved.
break
;;
bulk-scan)
if [ "$bulk_scan_command" = yes ]; then
bulk_scan_input=$argument
else
bulk_scan_command=yes
fi
;;
""|-*)
--output-dir|--workers|--mode|--model|--effort|--provider|\
--knowledge-base|--max-attempts|--plugin-path|--python|\
--codex|--filter-output|--format|\
--scan-prompt-file|--post-scan-prompt-file|\
--token-limit|--token-offset)
Comment thread
mldangelo-oai marked this conversation as resolved.
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
;;
Expand Down
10 changes: 10 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,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 bulk-scan repositories.csv --output-dir /path/outside/repositories/security-scans --scan-prompt-file scan.md --post-scan-prompt-file follow-up.md
npx @openai/codex-security scans list /path/to/repository
Expand Down Expand Up @@ -478,6 +479,11 @@ Private checkouts reuse your GitHub CLI sign-in without changing your global Git
configuration. The selected repositories are saved to
`<output-dir>/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`, `mode`, and `prompt` columns customize individual scans:
Expand Down Expand Up @@ -633,6 +639,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:

Expand Down
31 changes: 2 additions & 29 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1424,36 +1424,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" ||
argument === "--scan-prompt-file" ||
argument === "--post-scan-prompt-file"
) {
optionIndex += 2;
} else if (
argument.startsWith("--model=") ||
argument.startsWith("--effort=") ||
argument.startsWith("--provider=") ||
argument.startsWith("--codex=") ||
argument.startsWith("--knowledge-base=") ||
argument.startsWith("--scan-prompt-file=") ||
argument.startsWith("--post-scan-prompt-file=")
) {
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(
Expand Down
23 changes: 23 additions & 0 deletions sdk/typescript/tests-ts/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,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",
Expand Down Expand Up @@ -944,6 +948,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,
Expand Down Expand Up @@ -2297,6 +2316,10 @@ describe("CLI", () => {
["bulk-scan", "--knowledge-base="],
"--knowledge-base must not be empty",
],
[
["bulk-scan", "--output-dir", "results", "--", "repositories.csv"],
"Unknown flag: --",
],
[["scan", ".", "--model="], "--model must not be empty"],
[
["scan", ".", "--provider", "openrouter"],
Expand Down
60 changes: 54 additions & 6 deletions sdk/typescript/tests-ts/container-entrypoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,34 @@ 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"],
["bulk-scan", "bulk-scan", "--output-dir", "/output"],
[
"--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 () => {
Expand Down Expand Up @@ -229,15 +257,35 @@ 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"],
["bulk-scan", "--scan-prompt-file", "prompt.md"],
["bulk-scan", "--post-scan-prompt-file", "post-prompt.md"],
["--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"]);
Expand Down
Loading