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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ npm install @openai/codex-security
npx @openai/codex-security login
npx @openai/codex-security scan .
npx @openai/codex-security scan . --model gpt-5.6-terra --effort high
npx @openai/codex-security scan . --scan-prompt-file scan.md --post-scan-prompt-file follow-up.md
npx @openai/codex-security scan . --mode deep --workers 2 --subagents 0 --stop-after-no-new 3 --max-discovery-runs 10
```

Expand Down Expand Up @@ -113,6 +114,11 @@ hardening.
Pass `--knowledge-base PATH` to share security documents with every repository;
repeat the option for multiple files or directories.

Use `--scan-prompt-file PATH` to add shared scan instructions, and add a `prompt`
CSV column for repository-specific instructions. Use
`--post-scan-prompt-file PATH` to run a follow-up after each completed,
validated scan.

For complete command help, runtime defaults, native multi-agent worker limits,
environment variables, deep-scan configuration, and SDK options, see the
[package README](sdk/typescript/README.md) and the
Expand Down
13 changes: 10 additions & 3 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ npx @openai/codex-security scan /path/to/repository --model gpt-5.6-terra
npx @openai/codex-security scan /path/to/repository --model gpt-5.6-terra --effort high
npx @openai/codex-security scan /path/to/repository --path src --path tests
npx @openai/codex-security scan /path/to/repository --knowledge-base /path/to/threat-models --knowledge-base /path/to/architecture.pdf
npx @openai/codex-security scan /path/to/repository --scan-prompt-file scan.md --post-scan-prompt-file follow-up.md
npx @openai/codex-security scan /path/to/repository --diff origin/main --json
npx @openai/codex-security scan /path/to/repository --output-dir /path/outside/repository/results
npx @openai/codex-security scan /path/to/repository --output-dir /path/outside/repository/results --archive-existing
Expand All @@ -212,6 +213,7 @@ 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 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
npx @openai/codex-security scans list --scan-root /path/outside/repository/results
npx @openai/codex-security scans show SCAN_ID
Expand Down Expand Up @@ -478,13 +480,18 @@ configuration. The selected repositories are saved to

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:
optional `scope`, `mode`, and `prompt` columns customize individual scans:

```csv
id,repository,revision,scope,mode
service,https://github.com/acme/service.git,0123456789abcdef0123456789abcdef01234567,src,standard
id,repository,revision,scope,mode,prompt
service,https://github.com/acme/service.git,0123456789abcdef0123456789abcdef01234567,src,standard,Focus on authentication and authorization.
```

Use `--scan-prompt-file PATH` to add instructions to a scan or every bulk scan.
Bulk scans append each repository's CSV `prompt` after the shared instructions.
Use `--post-scan-prompt-file PATH` to run a follow-up in the same authenticated
session after each completed scan has been validated.

`--workers` limits concurrent scans and `--max-attempts` retries failures.
Results remain under `--output-dir`; rerun the same command to resume.

Expand Down
28 changes: 28 additions & 0 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ export interface ScanOptions extends DeepScanOptions {
target?: ScanTarget;
mode?: ScanMode;
knowledgeBasePaths?: string[];
scanPrompt?: string;
postScanPrompt?: string;
outputDir?: string;
archiveExisting?: boolean;
parentScanId?: string;
Expand Down Expand Up @@ -639,6 +641,7 @@ export class CodexSecurity {
mode,
runtime.configPath !== undefined,
knowledgeBase !== null,
options.scanPrompt,
);
checkOpen();
const expectation: ScanExpectation = {
Expand Down Expand Up @@ -1038,6 +1041,27 @@ export class CodexSecurity {
}
}
}
if (
options.postScanPrompt?.trim() &&
result.coverage.completeness === "complete"
) {
const followUp = await thread.runStreamed(options.postScanPrompt, {
signal,
});
await runScanEvents({
thread,
events: followUp.events,
signal,
scanDir,
pluginRoot: runtime.plugin.installedRoot,
expectation,
model,
onReconnect: options.onReconnect,
onWorkerStatus: options.onWorkerStatus,
onObserverError: options.onObserverError,
});
checkOpen();
}
return result;
} catch (error) {
// Recorded first: everything below can throw a different error for this same failed
Expand Down Expand Up @@ -1955,6 +1979,7 @@ async function scanPrompt(
mode: ScanMode,
hasConfigPath = false,
hasKnowledgeBase = false,
additionalPrompt?: string,
): Promise<string> {
const skillName = skillNameFor(target, mode);
const skillPath = join(pluginRoot, "skills", skillName, "SKILL.md");
Expand Down Expand Up @@ -2009,6 +2034,9 @@ async function scanPrompt(
"Runtime paths are environment-backed; keep them quoted in POSIX shells and use the corresponding $env: names in PowerShell. Do not copy or reparse their values.",
targetInstruction(target),
"Write the complete canonical scan-manifest.json, findings.json, and coverage.json, but do not finalize or seal them; the SDK workbench owns authoritative metadata, finalization, report generation, and sealing.",
...(additionalPrompt?.trim()
? ["Additional scan instructions:", additionalPrompt]
: []),
].join("\n");
}

Expand Down
57 changes: 55 additions & 2 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ const VALUE_OPTIONS = new Set([
"--auth",
"--path",
"--knowledge-base",
"--scan-prompt-file",
"--post-scan-prompt-file",
"--diff",
"--head",
"--base",
Expand Down Expand Up @@ -247,12 +249,33 @@ const DEEP_SCAN_OPTION_SCHEMAS = {
.describe("Maximum deep-scan discovery runs."),
};

async function readPromptFiles(
directory: string,
scanPromptFile?: string,
postScanPromptFile?: string,
): Promise<Pick<ScanOptions, "scanPrompt" | "postScanPrompt">> {
const [scanPrompt, postScanPrompt] = await Promise.all([
scanPromptFile === undefined
? undefined
: readFile(resolve(directory, scanPromptFile), "utf8"),
postScanPromptFile === undefined
? undefined
: readFile(resolve(directory, postScanPromptFile), "utf8"),
]);
return {
...(scanPrompt?.trim() ? { scanPrompt } : {}),
...(postScanPrompt?.trim() ? { postScanPrompt } : {}),
};
}

interface ScanArguments extends DeepScanOptions {
auth?: ScanAuthMode;
verbose?: boolean;
repository?: string;
paths: string[];
knowledgeBasePaths: string[];
scanPromptFile?: string;
postScanPromptFile?: string;
diff?: string;
workingTree: boolean;
head?: string;
Expand Down Expand Up @@ -1042,6 +1065,12 @@ export async function main(
.describe(
"Add security-context files or directories; repeat for multiple paths.",
),
scanPromptFile: optionValue("--scan-prompt-file")
.optional()
.describe("Append scan instructions from FILE."),
postScanPromptFile: optionValue("--post-scan-prompt-file")
.optional()
.describe("Run instructions from FILE after a validated scan."),
diff: optionValue("--diff")
.optional()
.describe("Scan committed Git changes from BASE to --head."),
Expand Down Expand Up @@ -1175,6 +1204,8 @@ export async function main(
repository: args.repository,
paths: options.path,
knowledgeBasePaths: options.knowledgeBase,
scanPromptFile: options.scanPromptFile,
postScanPromptFile: options.postScanPromptFile,
diff: options.diff,
workingTree: options.workingTree,
head: options.head,
Expand Down Expand Up @@ -1328,6 +1359,12 @@ export async function main(
.enum(["standard", "deep"])
.default("standard")
.describe("Default scan mode for repositories without a CSV mode."),
scanPromptFile: optionValue("--scan-prompt-file")
.optional()
.describe("Append instructions from FILE to every scan."),
postScanPromptFile: optionValue("--post-scan-prompt-file")
.optional()
.describe("Run FILE after each completed, validated scan."),
model: optionValue("--model")
.optional()
.describe(
Expand Down Expand Up @@ -1378,6 +1415,11 @@ export async function main(
dependencies.addSignalListener("SIGTERM", onTerminate);
try {
const currentDirectory = dependencies.currentDirectory();
const prompts = await readPromptFiles(
currentDirectory,
options.scanPromptFile,
options.postScanPromptFile,
);
let inputPath: string;
let outputDir: string;
let githubHost: string | undefined;
Expand All @@ -1390,15 +1432,19 @@ export async function main(
argument === "--effort" ||
argument === "--provider" ||
argument === "--codex" ||
argument === "--knowledge-base"
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("--knowledge-base=") ||
argument.startsWith("--scan-prompt-file=") ||
argument.startsWith("--post-scan-prompt-file=")
) {
optionIndex += 1;
} else {
Expand Down Expand Up @@ -1440,6 +1486,7 @@ export async function main(
mode: options.mode,
maxAttempts: options.maxAttempts,
knowledgeBasePaths: options.knowledgeBase,
...prompts,
config: {
pluginPath: options.pluginPath,
pythonPath: options.python,
Expand Down Expand Up @@ -2717,6 +2764,11 @@ async function runScan(
try {
const repository = arguments_.repository ?? dependencies.currentDirectory();
const target = targetFromArguments(arguments_);
const prompts = await readPromptFiles(
dependencies.currentDirectory(),
arguments_.scanPromptFile,
arguments_.postScanPromptFile,
);
const config: CodexSecurityConfig = {
pluginPath: arguments_.pluginPath,
pythonPath: arguments_.pythonPath,
Expand Down Expand Up @@ -2868,6 +2920,7 @@ async function runScan(
auth,
target,
knowledgeBasePaths: arguments_.knowledgeBasePaths,
...prompts,
mode: arguments_.mode,
workers: arguments_.workers,
subagents: arguments_.subagents,
Expand Down
30 changes: 28 additions & 2 deletions sdk/typescript/src/multiscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface MultiscanTask {
revision: string;
mode: ScanMode;
scope?: string;
prompt?: string;
}

interface MultiscanReceipt extends MultiscanTask {
Expand All @@ -53,6 +54,8 @@ export interface MultiscanOptions {
workers: number;
mode: ScanMode;
maxAttempts: number;
scanPrompt?: string;
postScanPrompt?: string;
config: CodexSecurityConfig;
createSecurity(
config: CodexSecurityConfig,
Expand Down Expand Up @@ -107,7 +110,7 @@ async function runCampaign(
const ledger = join(output, "results.jsonl");
await ensureOutputDirectory(join(output, "checkouts"));
await ensureOutputDirectory(join(output, "artifacts"));
await ensureManifest(join(output, "manifest.json"), tasks);
await ensureManifest(join(output, "manifest.json"), tasks, options);
const receipts = await readReceipts(ledger);
const pending: MultiscanTask[] = [];
let completed = 0;
Expand Down Expand Up @@ -180,13 +183,20 @@ async function runCampaign(
throw new Error("Multiscan scope escapes its repository.");
}
}
const scanPrompt = [options.scanPrompt?.trim(), task.prompt]
.filter(Boolean)
.join("\n\n");
const result = await security.run(checkout, {
...(task.scope === undefined ? {} : { target: [task.scope] }),
...(options.knowledgeBasePaths?.length
? { knowledgeBasePaths: options.knowledgeBasePaths }
: {}),
mode: task.mode,
outputDir: scanDir,
...(scanPrompt ? { scanPrompt } : {}),
...(options.postScanPrompt === undefined
? {}
: { postScanPrompt: options.postScanPrompt }),
...(options.signal === undefined ? {} : { signal: options.signal }),
});
cost = result.cost;
Expand Down Expand Up @@ -300,8 +310,22 @@ async function acquireLock(output: string): Promise<() => Promise<void>> {
async function ensureManifest(
path: string,
tasks: MultiscanTask[],
options: Pick<MultiscanOptions, "scanPrompt" | "postScanPrompt">,
): Promise<void> {
const expected = `${JSON.stringify({ version: 1, tasks }, null, 2)}\n`;
const expected = `${JSON.stringify(
{
version: 1,
tasks,
...(options.scanPrompt === undefined
? {}
: { scanPrompt: options.scanPrompt }),
...(options.postScanPrompt === undefined
? {}
: { postScanPrompt: options.postScanPrompt }),
},
null,
2,
)}\n`;
try {
await writeFile(path, expected, { flag: "wx", mode: 0o600 });
} catch (error) {
Expand Down Expand Up @@ -399,6 +423,7 @@ function parseInventory(
throw new Error("Multiscan mode must be standard or deep.");
}
const scope = get("scope");
const prompt = get("prompt");
if (
scope &&
(isAbsolute(scope) ||
Expand All @@ -414,6 +439,7 @@ function parseInventory(
revision,
mode,
...(scope ? { scope } : {}),
...(prompt ? { prompt } : {}),
};
});
}
Expand Down
Loading
Loading