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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ directory outside the repository.
`findings list [repository]` shows open findings across a repository's scans
and identifies findings not confirmed in its latest scan.

Use `patch --linear-issue SEC-123` to import and fix a Linear issue, or
`patch --linear-project "Security backlog" --linear-filter '{"labels":{"name":{"eq":"security"}}}'`
to fix matching open issues from a project. Set
`CODEX_SECURITY_LINEAR_API_KEY` to authorize read-only Linear access.

`scans compare BEFORE_SCAN_ID AFTER_SCAN_ID` automatically matches findings by
root cause, reuses saved matches, and identifies new, persisting, reopened,
resolved, or unknown findings. Missing findings remain unknown when coverage is
Expand All @@ -89,8 +94,9 @@ npx @openai/codex-security publish scan /path/to/scan \
--linear-team TEAM_ID
```

Add `--project PROJECT_ID` to place the issues in a Linear project, or omit it
to create issues directly in the team. Omit the scan directory to select a
Add `--linear-project PROJECT_ID` to place the issues in a Linear project, or
omit it to create issues directly in the team. The existing `--project` flag
remains an alias. Omit the scan directory to select a
completed scan interactively. You can also set `CODEX_SECURITY_LINEAR_TEAM` and
the optional `CODEX_SECURITY_LINEAR_PROJECT` instead of passing the destination
flags. Add `--dry-run` to preview the issues or `--json` to return
Expand All @@ -107,7 +113,7 @@ export CODEX_SECURITY_LINEAR_API_KEY=YOUR_LINEAR_PERSONAL_API_KEY
npx @openai/codex-security publish scan /path/to/scan \
--to linear \
--linear-team TEAM_ID \
--project PROJECT_ID \
--linear-project PROJECT_ID \
--linear-assignee teammate@example.com
```

Expand Down
22 changes: 19 additions & 3 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ npx @openai/codex-security validate /path/outside/repository/findings.json "Poss
npx @openai/codex-security validate "Possible SQL injection" --effort high
npx @openai/codex-security patch /path/outside/repository/findings.json "Missing authorization check in src/routes.ts:18"
npx @openai/codex-security patch "Missing authorization check" --effort high
npx @openai/codex-security patch --linear-issue SEC-123 --linear-issue SEC-124
npx @openai/codex-security patch --linear-project "Security backlog" --linear-filter '{"labels":{"name":{"eq":"security"}}}'
```

Run `npx @openai/codex-security --version` for the installed CLI version or
Expand Down Expand Up @@ -435,7 +437,7 @@ The CLI and SDK recognize the following user-configurable environment:
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `OPENAI_API_KEY`, `CODEX_API_KEY` | Scan authentication; `OPENAI_API_KEY` wins when both are present. |
| `CODEX_SECURITY_LINEAR_TEAM`, `CODEX_SECURITY_LINEAR_PROJECT` | Default Linear team and project for completed-scan publication. |
| `CODEX_SECURITY_LINEAR_API_KEY` | Publish directly to Linear with a personal API key; safer than a command-line key. |
| `CODEX_SECURITY_LINEAR_API_KEY` | Patch Linear issues or publish directly with a personal API key. |
| `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. |
Expand Down Expand Up @@ -559,8 +561,9 @@ npx @openai/codex-security publish scan /path/to/completed-scan \
--linear-team TEAM_ID
```

Add `--project PROJECT_ID` to place the issues in a Linear project. Without a
project, issues are created directly in the selected team.
Add `--linear-project PROJECT_ID` to place the issues in a Linear project.
The existing `--project` flag remains an alias. Without a project, issues are
created directly in the selected team.

To choose from all completed scans saved in your local scan history, omit the
scan directory. The selector highlights each repository and shows its finding
Expand Down Expand Up @@ -757,6 +760,19 @@ print the final response without the underlying Codex event stream. Override
the model with `--codex 'model="gpt-5.6-sol"'` and the reasoning effort with
`--effort high` or `--codex 'model_reasoning_effort="high"'`.

Use `patch --linear-issue ISSUE` to import a Linear issue by identifier or URL.
Repeat `--linear-issue` to include more issues. Use
`patch --linear-project "PROJECT"` to patch every open issue in a project. Add
`--linear-filter '{"labels":{"name":{"eq":"security"}}}'` to apply a native
Linear issue filter on the server. Completed and canceled issues are excluded
unless the filter explicitly sets `state`. Set `CODEX_SECURITY_LINEAR_API_KEY`
for a personal API key, or `LINEAR_ACCESS_TOKEN` for an OAuth access token.
`LINEAR_API_KEY` is also accepted. `--linear-api-key KEY` overrides these
environment settings; prefer the environment variable to keep keys out of shell
history. Imported content is always literal, and issue URLs must match the
selected workspace. Linear access is read-only, and its credentials are not
passed to the patch subprocess.

Exit codes are `0` for a completed report-only scan or a passing policy, `1`
for a completed policy violation, `2` for invalid input, incomplete coverage, or
a runtime/export error, `130` for interruption, and `143` for termination.
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/scripts/check-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ const distFiles = new Set(
"errors",
"index",
"knowledge-base",
"linear",
"models",
"multiscan",
"publication",
Expand Down
129 changes: 112 additions & 17 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ import {
ScanInterruptedError,
} from "./errors.js";
import type { SeverityLevel } from "./models.js";
import {
importLinearIssues,
resolveLinearApiKey,
type ImportedIssue,
type LinearClientFactory,
} from "./linear.js";
import { runMultiscan } from "./multiscan.js";
import {
publishScan,
Expand Down Expand Up @@ -204,6 +210,9 @@ const VALUE_OPTIONS = new Set([
"--plugin-path",
"--python",
"--codex",
"--linear-issue",
"--linear-project",
"--linear-filter",
"--fail-on-severity",
"--max-cost",
"--workers",
Expand Down Expand Up @@ -236,6 +245,17 @@ function optionValue(flag: string) {
return z.string().min(1, `${flag} must not be empty.`);
}

function linearApiKeyOption() {
return z
.string()
.trim()
.min(1, "--linear-api-key must not be empty.")
.optional()
.describe(
"Linear personal API key; defaults to CODEX_SECURITY_LINEAR_API_KEY.",
);
}

function publicationScanAge(timestamp: string, now: number): string {
const completedAt = Date.parse(timestamp);
if (!Number.isFinite(completedAt)) return "unknown";
Expand Down Expand Up @@ -715,6 +735,7 @@ interface CliDependencies {
environment?: NodeJS.ProcessEnv,
): Promise<number>;
bulkScan?: BulkScanDiscoveryDependencies;
linearClient?: LinearClientFactory;
runWorkbench(args: readonly string[]): Promise<JsonObject>;
matchFindings: typeof matchScanFindings;
checkForUpdate(signal: AbortSignal): Promise<UpdateNotice | undefined>;
Expand Down Expand Up @@ -1540,16 +1561,16 @@ export async function main(
linearTeam: optionValue("--linear-team")
.optional()
.describe("Linear team ID; defaults to CODEX_SECURITY_LINEAR_TEAM."),
linearApiKey: optionValue("--linear-api-key")
linearApiKey: linearApiKeyOption(),
linearProject: optionValue("--linear-project")
.optional()
.describe(
"Linear personal API key; defaults to CODEX_SECURITY_LINEAR_API_KEY.",
"Optional Linear project ID; defaults to CODEX_SECURITY_LINEAR_PROJECT.",
),
project: optionValue("--project")
.optional()
.describe(
"Optional Linear project ID; defaults to CODEX_SECURITY_LINEAR_PROJECT.",
),
.describe("Alias for --linear-project.")
.meta({ deprecated: true }),
linearAssignee: optionValue("--linear-assignee")
.optional()
.describe(
Expand All @@ -1572,13 +1593,10 @@ export async function main(
const onTerminate = (): void => cancel("SIGTERM");
let observingSignals = false;
try {
const selectedApiKey =
options.linearApiKey ??
dependencies.environment["CODEX_SECURITY_LINEAR_API_KEY"];
const linearApiKey = selectedApiKey?.trim() || undefined;
if (options.linearApiKey !== undefined && linearApiKey === undefined) {
throw new CodexSecurityError("--linear-api-key must not be empty.");
}
const linearApiKey = resolveLinearApiKey(
dependencies.environment,
options.linearApiKey,
);
const assigneeId = options.linearAssignee?.trim();
if (options.linearAssignee !== undefined && !assigneeId) {
throw new CodexSecurityError("--linear-assignee must not be empty.");
Expand All @@ -1596,9 +1614,21 @@ export async function main(
"--linear-team or CODEX_SECURITY_LINEAR_TEAM is required.",
);
}
const selectedProject = options.project?.trim();
if (options.project !== undefined && !selectedProject) {
throw new CodexSecurityError("--project must not be empty.");
if (
options.linearProject !== undefined &&
options.project !== undefined &&
options.linearProject.trim() !== options.project.trim()
) {
throw new CodexSecurityError(
"--linear-project and --project must select the same project.",
);
}
const projectOption = options.linearProject ?? options.project;
const selectedProject = projectOption?.trim();
if (projectOption !== undefined && !selectedProject) {
throw new CodexSecurityError(
`${options.linearProject === undefined ? "--project" : "--linear-project"} must not be empty.`,
);
}
const projectId =
selectedProject ||
Expand Down Expand Up @@ -2450,10 +2480,22 @@ export async function main(
"issues...": z
.string()
.min(1, "An issue must not be empty.")
.optional()
.describe("Issue text or a file containing issues."),
}),
options: z.object({
effort: effortOption(),
linearIssue: z
.array(optionValue("--linear-issue"))
.default([])
.describe("Linear issue identifier or URL; repeat for more issues."),
linearProject: optionValue("--linear-project")
.optional()
.describe("Patch every open issue in this Linear project."),
linearFilter: optionValue("--linear-filter")
.optional()
.describe("JSON Linear issue filter for --linear-project."),
linearApiKey: linearApiKeyOption(),
codex: z
.array(optionValue("--codex"))
.default([])
Expand All @@ -2463,14 +2505,59 @@ export async function main(
}),
async run({ options }) {
try {
const linear =
options.linearIssue.length > 0 || !!options.linearProject;
if (options.linearIssue.length > 0 && options.linearProject) {
throw new CodexSecurityError(
"Use either --linear-issue or --linear-project, not both.",
);
}
if (options.linearFilter && !options.linearProject) {
throw new CodexSecurityError(
"--linear-filter requires --linear-project.",
);
}
if (options.linearApiKey !== undefined && !linear) {
throw new CodexSecurityError(
"--linear-api-key requires --linear-issue or --linear-project.",
);
}
if (positionals.length === 0 && !linear) {
throw new CodexSecurityError(
"Patch requires an issue, --linear-issue, or --linear-project.",
);
}

const imports = linear
? await importLinearIssues({
issues: options.linearIssue,
project: options.linearProject,
filter: options.linearFilter,
apiKey: options.linearApiKey,
environment: dependencies.environment,
linearClient: dependencies.linearClient,
})
: [];
const environment =
imports.length === 0
? undefined
: Object.fromEntries(
Object.entries(dependencies.environment).filter(
([name]) =>
!/^(?:CODEX_SECURITY_)?LINEAR_(?:API_KEY|ACCESS_TOKEN)$/iu.test(
name,
),
),
);
exitCode = await runSkill(
"fix-finding",
positionals,
[...positionals, ...imports],
Comment thread
ianw-oai marked this conversation as resolved.
options.codex,
options.effort,
output,
errorOutput,
dependencies,
environment,
);
} catch (error) {
exitCode = 2;
Expand Down Expand Up @@ -3101,12 +3188,13 @@ function staysWithinWindowsDeviceRoot(input: string, root: string): boolean {

async function runSkill(
skill: "validation" | "fix-finding",
inputs: readonly string[],
inputs: readonly (string | ImportedIssue)[],
codexOverrides: readonly string[],
effort: ScanReasoningEffort | undefined,
stdout: Writable,
stderr: Writable,
dependencies: CliDependencies,
environment?: NodeJS.ProcessEnv,
): Promise<number> {
const overrides = parseCodexOverrides(codexOverrides, undefined, effort);
if (
Expand All @@ -3124,6 +3212,12 @@ async function runSkill(
const directory = dependencies.currentDirectory();
const contents: string[] = [];
for (const input of inputs) {
if (typeof input !== "string") {
contents.push(
`Source: ${input.source}\nIssue: ${input.id}\nURL: ${input.url}\n\n${input.text}`,
);
continue;
}
if (input.trim().length === 0) {
throw new CodexSecurityError(
"Finding or issue inputs must not be empty.",
Expand Down Expand Up @@ -3226,6 +3320,7 @@ async function runSkill(
stdout,
stderr,
},
environment,
);
}

Expand Down
Loading
Loading