codex-plugin-doctor 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -402,9 +402,9 @@ jobs:
402
402
  runs-on: ubuntu-latest
403
403
  steps:
404
404
  - uses: actions/checkout@v5
405
- - uses: Esquetta/CodexPluginDoctor@v1.37.0
405
+ - uses: Esquetta/CodexPluginDoctor@v1.38.0
406
406
  with:
407
- version: "1.37.0"
407
+ version: "1.38.0"
408
408
  path: .
409
409
  runtime: "true"
410
410
  policy: codex-publish
@@ -15,9 +15,13 @@ async function fileExists(filePath) {
15
15
  }
16
16
  async function runNpmAudit(cwd) {
17
17
  return new Promise((resolve, reject) => {
18
- const command = process.platform === "win32" ? process.env.ComSpec ?? "cmd.exe" : "npm";
18
+ const npmCliPath = process.env.npm_execpath ??
19
+ path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
20
+ const command = process.platform === "win32"
21
+ ? process.execPath
22
+ : "npm";
19
23
  const args = process.platform === "win32"
20
- ? ["/d", "/s", "/c", "npm", "audit", "--json"]
24
+ ? [npmCliPath, "audit", "--json"]
21
25
  : ["audit", "--json"];
22
26
  execFile(command, args, { cwd, timeout: 120_000 }, (error, stdout, stderr) => {
23
27
  const parsed = (() => {
@@ -9,9 +9,11 @@ const execFileAsync = promisify(execFile);
9
9
  const gunzipAsync = promisify(gunzip);
10
10
  function npmCommand(args) {
11
11
  if (process.platform === "win32") {
12
+ const npmCliPath = process.env.npm_execpath ??
13
+ path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
12
14
  return {
13
- command: process.env.ComSpec ?? "cmd.exe",
14
- args: ["/d", "/s", "/c", "npm", ...args]
15
+ command: process.execPath,
16
+ args: [npmCliPath, ...args]
15
17
  };
16
18
  }
17
19
  return { command: "npm", args };
@@ -13,6 +13,7 @@ export interface SecurityAudit {
13
13
  export declare function auditMcpServerConfig(rootPath: string, parsedConfig: unknown, options?: {
14
14
  configPath?: string;
15
15
  }): Finding[];
16
+ export declare function auditChildProcessSourceSurface(rootPath: string): Promise<Finding[]>;
16
17
  export declare function buildSecurityAuditFromFindings(targetPath: string, findings: Finding[]): SecurityAudit;
17
18
  export declare function buildSecurityAudit(targetPath: string): Promise<SecurityAudit>;
18
19
  export declare function renderSecurityAuditJson(audit: SecurityAudit): string;
@@ -157,13 +157,34 @@ const poisonScanSkippedDirectories = new Set([
157
157
  ".git",
158
158
  "coverage",
159
159
  "dist",
160
- "node_modules"
160
+ "node_modules",
161
+ "release-candidate",
162
+ "tests"
161
163
  ]);
162
164
  const promptInjectionPatterns = [
163
165
  /\bignore\s+(?:all\s+)?(?:previous|prior|system|developer)\s+instructions?\b/i,
164
166
  /\b(?:exfiltrate|steal|leak|upload|send)\b.{0,120}\b(?:secret|secrets|token|tokens|api\s*key|api\s*keys|credential|credentials|environment\s+variables?|env)\b/i,
165
167
  /\bdo\s+not\s+(?:reveal|tell|mention|disclose)\b.{0,120}\b(?:instruction|instructions|prompt|prompts|system|developer)\b/i
166
168
  ];
169
+ const childProcessScanExtensions = new Set([
170
+ ".cjs",
171
+ ".js",
172
+ ".mjs",
173
+ ".ts",
174
+ ".tsx"
175
+ ]);
176
+ function importsChildProcess(content) {
177
+ return (/\bfrom\s+["']node:child_process["']/.test(content) ||
178
+ /\bfrom\s+["']child_process["']/.test(content) ||
179
+ /\brequire\(\s*["'](?:node:)?child_process["']\s*\)/.test(content));
180
+ }
181
+ function sourceUsesShellOption(content) {
182
+ return /\bshell\s*:\s*(?:true|process\.platform\s*===\s*["']win32["'])/.test(content);
183
+ }
184
+ function sourceUsesShellCommand(content) {
185
+ return (/["']cmd(?:\.exe)?["'][\s\S]{0,200}["']\/c["']/i.test(content) ||
186
+ /["'](?:powershell|pwsh)(?:\.exe)?["'][\s\S]{0,200}["']-(?:command|encodedcommand|enc)["']/i.test(content));
187
+ }
167
188
  export function auditMcpServerConfig(rootPath, parsedConfig, options = {}) {
168
189
  const configPath = options.configPath
169
190
  ? relativePackagePath(rootPath, options.configPath)
@@ -270,6 +291,45 @@ async function auditPromptPoisoningSurface(rootPath) {
270
291
  }
271
292
  return findings;
272
293
  }
294
+ async function collectChildProcessScanFiles(rootPath, currentPath = rootPath) {
295
+ const entries = await readdir(currentPath, { withFileTypes: true });
296
+ const filePaths = [];
297
+ for (const entry of entries) {
298
+ const entryPath = path.join(currentPath, entry.name);
299
+ if (entry.isDirectory()) {
300
+ if (poisonScanSkippedDirectories.has(entry.name)) {
301
+ continue;
302
+ }
303
+ filePaths.push(...(await collectChildProcessScanFiles(rootPath, entryPath)));
304
+ continue;
305
+ }
306
+ if (!entry.isFile() || !childProcessScanExtensions.has(path.extname(entry.name).toLowerCase())) {
307
+ continue;
308
+ }
309
+ const details = await stat(entryPath);
310
+ if (details.size <= 512 * 1024) {
311
+ filePaths.push(entryPath);
312
+ }
313
+ }
314
+ return filePaths;
315
+ }
316
+ export async function auditChildProcessSourceSurface(rootPath) {
317
+ const findings = [];
318
+ for (const filePath of await collectChildProcessScanFiles(rootPath)) {
319
+ const content = await readFile(filePath, "utf8");
320
+ if (!importsChildProcess(content)) {
321
+ continue;
322
+ }
323
+ const relativeFilePath = relativePackagePath(rootPath, filePath);
324
+ if (sourceUsesShellOption(content)) {
325
+ findings.push(buildFinding("fail", "plugin.security.child_process_shell", `The source file \`${relativeFilePath}\` enables child_process shell execution.`, "Shell execution expands command strings through a platform shell, which can turn quoting bugs or user-controlled args into command injection.", "Use `spawn` or `execFile` with an explicit executable and argument array instead of enabling `shell`.", { filePath: relativeFilePath }));
326
+ }
327
+ if (sourceUsesShellCommand(content)) {
328
+ findings.push(buildFinding("fail", "plugin.security.child_process_shell_command", `The source file \`${relativeFilePath}\` appears to launch a shell command wrapper.`, "Explicit shell wrappers such as `cmd /c` or `powershell -Command` make command execution harder to audit and increase injection risk.", "Launch the concrete executable directly, or use platform-specific executable names such as `npm.cmd` on Windows.", { filePath: relativeFilePath }));
329
+ }
330
+ }
331
+ return findings;
332
+ }
273
333
  async function auditMcpCommandSurface(discoveredPackage) {
274
334
  const { manifest, rootPath } = discoveredPackage;
275
335
  if (!manifest.mcpServers) {
@@ -343,6 +403,7 @@ export async function buildSecurityAudit(targetPath) {
343
403
  const findings = [
344
404
  ...validationSecurityFindings,
345
405
  ...(await auditMcpCommandSurface(discoveredPackage)),
406
+ ...(await auditChildProcessSourceSurface(discoveredPackage.rootPath)),
346
407
  ...(await auditPromptPoisoningSurface(discoveredPackage.rootPath))
347
408
  ];
348
409
  return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",