codex-plugin-doctor 1.36.1 → 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 +2 -2
- package/dist/core/dep-audit.js +6 -2
- package/dist/core/npm-package-doctor.js +4 -2
- package/dist/core/validate-plugin.js +1 -0
- package/dist/domain/types.d.ts +1 -0
- package/dist/security/security-audit.d.ts +1 -0
- package/dist/security/security-audit.js +62 -1
- package/package.json +1 -1
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.
|
|
405
|
+
- uses: Esquetta/CodexPluginDoctor@v1.38.0
|
|
406
406
|
with:
|
|
407
|
-
version: "1.
|
|
407
|
+
version: "1.38.0"
|
|
408
408
|
path: .
|
|
409
409
|
runtime: "true"
|
|
410
410
|
policy: codex-publish
|
package/dist/core/dep-audit.js
CHANGED
|
@@ -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
|
|
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
|
-
? [
|
|
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.
|
|
14
|
-
args: [
|
|
15
|
+
command: process.execPath,
|
|
16
|
+
args: [npmCliPath, ...args]
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
return { command: "npm", args };
|
|
@@ -406,6 +406,7 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
406
406
|
}
|
|
407
407
|
const runtimeResult = options.runtime
|
|
408
408
|
? await probeRuntime(discoveredPackage, {
|
|
409
|
+
startupTimeoutMs: options.runtimeStartupTimeoutMs,
|
|
409
410
|
transcript: options.runtimeTranscript
|
|
410
411
|
})
|
|
411
412
|
: null;
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -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