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
2 changes: 1 addition & 1 deletion sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-security",
"version": "0.1.20",
"version": "0.1.21",
"description": "Codex Security workflows for security scans, analysis, and investigation.",
"author": {
"name": "OpenAI"
Expand Down
9 changes: 8 additions & 1 deletion sdk/typescript/_bundled_plugin/scripts/workbench_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,18 @@ def git_command(
for name in GIT_REPOSITORY_ENVIRONMENT:
environment.pop(name, None)
environment["GIT_LITERAL_PATHSPECS"] = "1"
executable = environment.pop("CODEX_SECURITY_GIT_EXECUTABLE", "git")
selected_path = environment.pop("CODEX_SECURITY_GIT_PATH", None)
if selected_path is not None:
environment["PATH"] = selected_path
# Repository-local config is untrusted; fsmonitor may name an executable hook.
command = ["git", "-c", "core.fsmonitor=false", "-C", str(target)]
command = [executable, "-c", "core.fsmonitor=false", "-C", str(target)]
if git_dir is not None and work_tree is not None:
command.extend(["--git-dir", str(git_dir), "--work-tree", str(work_tree)])
full_command = [*command, *args]
if not executable:
empty_output = "" if text else b""
return subprocess.CompletedProcess(full_command, 127, empty_output, empty_output)
try:
return subprocess.run(
full_command,
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@ export class CodexSecurity {
const workbenchOptions: WorkbenchCommandOptions = {
python,
pluginRoot: runtime.plugin.pluginRoot,
protectedRoot,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate trusted Git to scan helper processes

When a diff scan is launched with a repository-local directory such as node_modules/.bin on PATH, protectedRoot sanitizes Git only for SDK-owned runWorkbench calls. The Codex thread environment built later in api.ts does not receive the selected executable or sanitized path, while security-diff-scan/SKILL.md invokes generate_in_scope_files.py and generate_rank_input.py, which still execute bare git commands. A repository-controlled shim can therefore run during discovery and falsify the changed-file inventory despite this fix; apply the trusted Git selection to those helper processes as well.

AGENTS.md reference: sdk/typescript/AGENTS.md:L19-L20

Useful? React with 👍 / 👎.

environment: {
...selectedScanEnvironment(
runtime.environment,
Expand Down
44 changes: 35 additions & 9 deletions sdk/typescript/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface WorkbenchCommandOptions {
python: string;
pluginRoot: string;
environment: ProcessEnvironment;
protectedRoot?: string;
signal?: AbortSignal;
failureMessage?: string;
}
Expand Down Expand Up @@ -1292,6 +1293,39 @@ export async function runWorkbench(
): Promise<JsonObject> {
let stdout: string;
try {
let environment = Object.fromEntries(
Object.entries(options.environment).filter(
([name]) =>
name.toUpperCase() !== "OPENAI_API_KEY" &&
name.toUpperCase() !== "CODEX_API_KEY" &&
name.toUpperCase() !== "OPENROUTER_API_KEY" &&
name.toUpperCase() !== "FIREWORKS_API_KEY",
),
);
if (options.protectedRoot !== undefined) {
if (
!Object.keys(environment).some((name) => name.toUpperCase() === "PATH")
) {
const inheritedPath = Object.entries(process.env).find(
([name]) => name.toUpperCase() === "PATH",
)?.[1];
if (inheritedPath !== undefined) environment["PATH"] = inheritedPath;
}
const python = await resolveTrustedExecutable(
options.python,
environment,
options.protectedRoot,
);
if (python !== null) environment = python.environment;
const git = await resolveTrustedExecutable(
"git",
environment,
options.protectedRoot,
);
environment["CODEX_SECURITY_GIT_EXECUTABLE"] = git?.executable ?? "";
environment["CODEX_SECURITY_GIT_PATH"] =
git?.environment["PATH"] ?? environment["PATH"] ?? "";
}
({ stdout } = await execFile(
options.python,
[
Expand All @@ -1301,15 +1335,7 @@ export async function runWorkbench(
...args,
],
{
env: Object.fromEntries(
Object.entries(options.environment).filter(
([name]) =>
name.toUpperCase() !== "OPENAI_API_KEY" &&
name.toUpperCase() !== "CODEX_API_KEY" &&
name.toUpperCase() !== "OPENROUTER_API_KEY" &&
name.toUpperCase() !== "FIREWORKS_API_KEY",
),
),
env: environment,
encoding: "utf8",
maxBuffer: Infinity,
windowsHide: true,
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const PACKAGE_VERSIONS = packageVersions(
export const VERSION = PACKAGE_VERSIONS.package;
export const CODEX_SDK_VERSION = PACKAGE_VERSIONS.sdk;
export const CODEX_EXECUTABLE_VERSION = PACKAGE_VERSIONS.executable;
export const BUNDLED_PLUGIN_VERSION = "0.1.20" as const;
export const BUNDLED_PLUGIN_VERSION = "0.1.21" as const;

const PACKAGE_NAME = "@openai/codex-security";
const VERSION_PATTERN =
Expand Down
107 changes: 107 additions & 0 deletions sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3809,6 +3809,113 @@ describe("runtime directories and plugin Python boundary", () => {
expect(result["details"]).toHaveLength(5 * 1024 * 1024);
});

test("does not execute repository-local Git shims in the trusted workbench", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const untrusted = join(repository, "node_modules", ".bin");
const pluginRoot = join(root, "plugin");
await mkdir(untrusted, { recursive: true });
await mkdir(join(pluginRoot, "scripts"), { recursive: true });
const fakeGit = join(
untrusted,
process.platform === "win32" ? "git.cmd" : "git",
);
await writeFile(
fakeGit,
process.platform === "win32"
? "@echo repository-controlled-git\r\n"
: "#!/bin/sh\necho repository-controlled-git\n",
{ mode: 0o755 },
);
await writeFile(
join(pluginRoot, "scripts", "workbench_db.py"),
[
"import json, os, sys",
"from pathlib import Path",
"sys.path.insert(0, os.environ['CODEX_SECURITY_TEST_PLUGIN_SCRIPTS'])",
"from workbench_target import git_output",
"git = git_output(Path(os.environ['CODEX_SECURITY_TEST_REPOSITORY']), '--version')",
"print(json.dumps({'git': git, 'path': os.environ.get('PATH')}))",
].join("\n"),
);
const python = Bun.which("python3") ?? Bun.which("python");
expect(python).not.toBeNull();

const result = await runWorkbench(
{
python: python!,
pluginRoot,
protectedRoot: repository,
environment: {
CODEX_SECURITY_TEST_PLUGIN_SCRIPTS: join(PLUGIN_ROOT, "scripts"),
CODEX_SECURITY_TEST_REPOSITORY: repository,
PATH: [untrusted, process.env["PATH"]]
.filter(Boolean)
.join(delimiter),
},
},
["test-command"],
);

expect(result["git"]).toStartWith("git version ");
expect(String(result["path"]).split(delimiter)).not.toContain(untrusted);

const withoutGit = await runWorkbench(
{
python: python!,
pluginRoot,
protectedRoot: repository,
environment: {
CODEX_SECURITY_TEST_PLUGIN_SCRIPTS: join(PLUGIN_ROOT, "scripts"),
CODEX_SECURITY_TEST_REPOSITORY: repository,
PATH: untrusted,
},
},
["test-command"],
);
expect(withoutGit).toMatchObject({ git: null, path: "" });

const inherited = await runWorkbench(
{
python: python!,
pluginRoot,
protectedRoot: repository,
environment: {
CODEX_SECURITY_TEST_PLUGIN_SCRIPTS: join(PLUGIN_ROOT, "scripts"),
CODEX_SECURITY_TEST_REPOSITORY: repository,
},
},
["test-command"],
);
expect(inherited["git"]).toStartWith("git version ");

if (process.platform !== "win32") {
const helpers = join(root, "trusted-helpers");
const launcher = join(root, "trusted-python");
await mkdir(helpers);
await symlink(Bun.which("sh")!, join(helpers, "sh"));
await writeFile(
launcher,
`#!/usr/bin/env sh\nexec ${JSON.stringify(python)} "$@"\n`,
{ mode: 0o755 },
);
const configured = await runWorkbench(
{
python: launcher,
pluginRoot,
protectedRoot: repository,
environment: {
CODEX_SECURITY_TEST_PLUGIN_SCRIPTS: join(PLUGIN_ROOT, "scripts"),
CODEX_SECURITY_TEST_REPOSITORY: repository,
PATH: helpers,
},
},
["test-command"],
);
expect(configured).toMatchObject({ git: null, path: helpers });
}
});

test("upgrades colliding legacy execution-profile and public CLI migrations", async () => {
const root = await temporaryDirectory("codex-security-legacy-migrations-");
const repository = join(root, "repository");
Expand Down
Loading