codex-plugin-doctor 1.37.0 → 1.39.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/init-git-hooks.js +75 -37
- package/dist/core/npm-package-doctor.js +4 -2
- package/dist/rules/rule-catalog.js +9 -0
- package/dist/run-cli.js +64 -1
- package/dist/security/security-audit.d.ts +1 -0
- package/dist/security/security-audit.js +81 -2
- 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.39.0
|
|
406
406
|
with:
|
|
407
|
-
version: "1.
|
|
407
|
+
version: "1.39.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 = (() => {
|
|
@@ -1,39 +1,77 @@
|
|
|
1
1
|
import { chmod, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
3
|
+
const isWindows = process.platform === "win32";
|
|
4
|
+
const preCommitHook = isWindows
|
|
5
|
+
? [
|
|
6
|
+
"@echo off",
|
|
7
|
+
"REM Generated by codex-plugin-doctor init git-hooks",
|
|
8
|
+
"REM Validates the plugin package before every commit.",
|
|
9
|
+
"",
|
|
10
|
+
"echo Codex Plugin Doctor: running pre-commit validation...",
|
|
11
|
+
"",
|
|
12
|
+
"call npx --yes codex-plugin-doctor check . --profile ci",
|
|
13
|
+
"",
|
|
14
|
+
"if %ERRORLEVEL% neq 0 (",
|
|
15
|
+
" echo.",
|
|
16
|
+
" echo Plugin validation failed. Commit blocked.",
|
|
17
|
+
" echo Run 'codex-plugin-doctor check .' to see full diagnostics.",
|
|
18
|
+
" exit /b 1",
|
|
19
|
+
")",
|
|
20
|
+
""
|
|
21
|
+
].join("\r\n")
|
|
22
|
+
: [
|
|
23
|
+
"#!/usr/bin/env sh",
|
|
24
|
+
"# Generated by codex-plugin-doctor init git-hooks",
|
|
25
|
+
"# Validates the plugin package before every commit.",
|
|
26
|
+
"",
|
|
27
|
+
"echo \"Codex Plugin Doctor: running pre-commit validation...\"",
|
|
28
|
+
"",
|
|
29
|
+
"npx --yes codex-plugin-doctor check . --profile ci",
|
|
30
|
+
"",
|
|
31
|
+
"if [ $? -ne 0 ]; then",
|
|
32
|
+
" echo \"\"",
|
|
33
|
+
" echo \"Plugin validation failed. Commit blocked.\"",
|
|
34
|
+
" echo \"Run 'codex-plugin-doctor check .' to see full diagnostics.\"",
|
|
35
|
+
" exit 1",
|
|
36
|
+
"fi",
|
|
37
|
+
""
|
|
38
|
+
].join("\n");
|
|
39
|
+
const prePushHook = isWindows
|
|
40
|
+
? [
|
|
41
|
+
"@echo off",
|
|
42
|
+
"REM Generated by codex-plugin-doctor init git-hooks",
|
|
43
|
+
"REM Validates the plugin package before every push.",
|
|
44
|
+
"",
|
|
45
|
+
"echo Codex Plugin Doctor: running pre-push validation...",
|
|
46
|
+
"",
|
|
47
|
+
"call npx --yes codex-plugin-doctor check . --profile ci --runtime",
|
|
48
|
+
"",
|
|
49
|
+
"if %ERRORLEVEL% neq 0 (",
|
|
50
|
+
" echo.",
|
|
51
|
+
" echo Plugin validation failed. Push blocked.",
|
|
52
|
+
" echo Run 'codex-plugin-doctor check . --runtime' to see full diagnostics.",
|
|
53
|
+
" exit /b 1",
|
|
54
|
+
")",
|
|
55
|
+
""
|
|
56
|
+
].join("\r\n")
|
|
57
|
+
: [
|
|
58
|
+
"#!/usr/bin/env sh",
|
|
59
|
+
"# Generated by codex-plugin-doctor init git-hooks",
|
|
60
|
+
"# Validates the plugin package before every push.",
|
|
61
|
+
"",
|
|
62
|
+
"echo \"Codex Plugin Doctor: running pre-push validation...\"",
|
|
63
|
+
"",
|
|
64
|
+
"npx --yes codex-plugin-doctor check . --profile ci --runtime",
|
|
65
|
+
"",
|
|
66
|
+
"if [ $? -ne 0 ]; then",
|
|
67
|
+
" echo \"\"",
|
|
68
|
+
" echo \"Plugin validation failed. Push blocked.\"",
|
|
69
|
+
" echo \"Run 'codex-plugin-doctor check . --runtime' to see full diagnostics.\"",
|
|
70
|
+
" exit 1",
|
|
71
|
+
"fi",
|
|
72
|
+
""
|
|
73
|
+
].join("\n");
|
|
74
|
+
const hookExtension = isWindows ? ".bat" : "";
|
|
37
75
|
async function fileExists(filePath) {
|
|
38
76
|
try {
|
|
39
77
|
await stat(filePath);
|
|
@@ -57,7 +95,7 @@ async function writeHook(hookPath, content, force) {
|
|
|
57
95
|
export async function removeGitHooks(targetPath) {
|
|
58
96
|
const rootPath = path.resolve(targetPath);
|
|
59
97
|
const hooksDir = path.join(rootPath, ".git", "hooks");
|
|
60
|
-
const hookNames = ["pre-commit", "pre-push"];
|
|
98
|
+
const hookNames = [`pre-commit${hookExtension}`, `pre-push${hookExtension}`, "pre-commit", "pre-push"];
|
|
61
99
|
const removed = [];
|
|
62
100
|
const skipped = [];
|
|
63
101
|
for (const name of hookNames) {
|
|
@@ -82,8 +120,8 @@ export async function initGitHooks(targetPath, options = {}) {
|
|
|
82
120
|
await mkdir(hooksDir, { recursive: true });
|
|
83
121
|
const hookPaths = [];
|
|
84
122
|
const preExisting = [];
|
|
85
|
-
const preCommitPath = path.join(hooksDir,
|
|
86
|
-
const prePushPath = path.join(hooksDir,
|
|
123
|
+
const preCommitPath = path.join(hooksDir, `pre-commit${hookExtension}`);
|
|
124
|
+
const prePushPath = path.join(hooksDir, `pre-push${hookExtension}`);
|
|
87
125
|
const wasPreCommitExisting = await writeHook(preCommitPath, preCommitHook, force);
|
|
88
126
|
hookPaths.push(preCommitPath);
|
|
89
127
|
if (wasPreCommitExisting) {
|
|
@@ -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 };
|
|
@@ -260,6 +260,15 @@ export const ruleCatalog = [
|
|
|
260
260
|
fix: "Use `127.0.0.1` or `localhost` in server URLs and transport configuration unless external access is explicitly required.",
|
|
261
261
|
example: '{ "url": "http://127.0.0.1:3000/mcp" }'
|
|
262
262
|
},
|
|
263
|
+
{
|
|
264
|
+
id: "plugin.skill.external_http_reference",
|
|
265
|
+
category: "skill",
|
|
266
|
+
defaultSeverity: "warn",
|
|
267
|
+
summary: "A packaged skill text references an external HTTP/HTTPS URL.",
|
|
268
|
+
why: "External URLs in skill instructions can lead to link rot, phishing risk, or unauthorized telemetry when an agent follows them.",
|
|
269
|
+
fix: "Replace the external URL with a local reference, or document the link only in the plugin README where reviewers can see it.",
|
|
270
|
+
example: "Replace `https://example.com/docs` with a local `docs/setup.md` reference."
|
|
271
|
+
},
|
|
263
272
|
{
|
|
264
273
|
id: "plugin.security.prompt_injection_text",
|
|
265
274
|
category: "security",
|
package/dist/run-cli.js
CHANGED
|
@@ -78,7 +78,7 @@ const defaultIo = {
|
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
80
|
function printUsage(io) {
|
|
81
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
81
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
82
82
|
io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
|
|
83
83
|
}
|
|
84
84
|
const suppressUsageText = [
|
|
@@ -2254,6 +2254,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2254
2254
|
const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
|
|
2255
2255
|
? null
|
|
2256
2256
|
: normalizedFlags[runtimeApprovalDigestIndex + 1];
|
|
2257
|
+
const changedSinceIndex = normalizedFlags.indexOf("--changed-since");
|
|
2258
|
+
const changedSinceRef = changedSinceIndex === -1 ? null : normalizedFlags[changedSinceIndex + 1];
|
|
2257
2259
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
2258
2260
|
io.writeStderr("Missing path after --output.");
|
|
2259
2261
|
return 2;
|
|
@@ -2282,6 +2284,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2282
2284
|
io.writeStderr("Missing path after --history.");
|
|
2283
2285
|
return 2;
|
|
2284
2286
|
}
|
|
2287
|
+
if (changedSinceIndex !== -1 && (!changedSinceRef || changedSinceRef.startsWith("--"))) {
|
|
2288
|
+
io.writeStderr("Missing ref after --changed-since.");
|
|
2289
|
+
return 2;
|
|
2290
|
+
}
|
|
2291
|
+
if (changedSinceIndex !== -1 && checkInstalled) {
|
|
2292
|
+
io.writeStderr("--changed-since requires a single package target.");
|
|
2293
|
+
return 2;
|
|
2294
|
+
}
|
|
2285
2295
|
if (runtimeApprovalDigestIndex !== -1 &&
|
|
2286
2296
|
(!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
|
|
2287
2297
|
io.writeStderr("Missing digest after --runtime-approval-digest.");
|
|
@@ -2384,6 +2394,59 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2384
2394
|
? createLiveStatusRenderer(io, getSpinner(outputPolicy.style === "ascii" ? "ascii" : "doctor"))
|
|
2385
2395
|
: null;
|
|
2386
2396
|
renderer?.start("Validating package");
|
|
2397
|
+
if (changedSinceRef) {
|
|
2398
|
+
const { execFile } = await import("node:child_process");
|
|
2399
|
+
try {
|
|
2400
|
+
const gitOutput = await new Promise((resolve, reject) => {
|
|
2401
|
+
execFile("git", ["diff", "--name-only", `${changedSinceRef}...HEAD`], { cwd: path.resolve(targetPath) }, (error, stdout, stderr) => {
|
|
2402
|
+
if (error) {
|
|
2403
|
+
reject(new Error(stderr.trim() || error.message));
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
resolve(stdout);
|
|
2407
|
+
});
|
|
2408
|
+
});
|
|
2409
|
+
const changedFiles = gitOutput.split("\n").filter(Boolean);
|
|
2410
|
+
if (changedFiles.length === 0) {
|
|
2411
|
+
io.writeStdout("No changed files since the given ref.");
|
|
2412
|
+
return 0;
|
|
2413
|
+
}
|
|
2414
|
+
const changedDirs = new Set();
|
|
2415
|
+
for (const file of changedFiles) {
|
|
2416
|
+
const dir = path.dirname(file);
|
|
2417
|
+
changedDirs.add(dir);
|
|
2418
|
+
}
|
|
2419
|
+
let allPassed = true;
|
|
2420
|
+
let validatedCount = 0;
|
|
2421
|
+
for (const dir of changedDirs) {
|
|
2422
|
+
const pluginRoot = path.resolve(targetPath, dir);
|
|
2423
|
+
const manifestPath = path.join(pluginRoot, ".codex-plugin", "plugin.json");
|
|
2424
|
+
try {
|
|
2425
|
+
await (await import("node:fs/promises")).stat(manifestPath);
|
|
2426
|
+
}
|
|
2427
|
+
catch {
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
const pluginResult = applyDoctorConfig(await runCheckImpl(pluginRoot, {
|
|
2431
|
+
runtime: effectiveRuntimeProbeEnabled
|
|
2432
|
+
}), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(pluginRoot, configPath), checkProfile), policy));
|
|
2433
|
+
io.writeStdout(`\n${pluginResult.targetPath}: ${pluginResult.status.toUpperCase()}`);
|
|
2434
|
+
validatedCount += 1;
|
|
2435
|
+
if (pluginResult.status !== "pass") {
|
|
2436
|
+
allPassed = false;
|
|
2437
|
+
for (const finding of pluginResult.findings) {
|
|
2438
|
+
io.writeStdout(` ${finding.severity === "fail" ? "FAIL" : "WARN"} ${finding.id}: ${finding.message}`);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
io.writeStdout(`\n\nValidated ${validatedCount} changed plugins.`);
|
|
2443
|
+
return allPassed ? 0 : 1;
|
|
2444
|
+
}
|
|
2445
|
+
catch (error) {
|
|
2446
|
+
io.writeStderr(`--changed-since requires a git repository. ${error.message}`);
|
|
2447
|
+
return 2;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2387
2450
|
const result = applyDoctorConfig(await runCheckImpl(targetPath, {
|
|
2388
2451
|
runtime: effectiveRuntimeProbeEnabled,
|
|
2389
2452
|
runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
|
|
@@ -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)
|
|
@@ -233,6 +254,23 @@ export function auditMcpServerConfig(rootPath, parsedConfig, options = {}) {
|
|
|
233
254
|
}
|
|
234
255
|
return findings;
|
|
235
256
|
}
|
|
257
|
+
const externalUrlPattern = /https?:\/\/[^\s`"'<>)]+/gi;
|
|
258
|
+
async function auditSkillExternalReferences(rootPath) {
|
|
259
|
+
const findings = [];
|
|
260
|
+
for (const filePath of await collectPromptPoisoningScanFiles(rootPath)) {
|
|
261
|
+
const content = await readFile(filePath, "utf8");
|
|
262
|
+
const matches = content.match(externalUrlPattern);
|
|
263
|
+
if (!matches) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const uniqueUrls = [...new Set(matches)];
|
|
267
|
+
const relativeFilePath = path.relative(rootPath, filePath).replace(/\\/g, "/");
|
|
268
|
+
for (const url of uniqueUrls.slice(0, 5)) {
|
|
269
|
+
findings.push(buildFinding("warn", "plugin.skill.external_http_reference", `The packaged text file \`${relativeFilePath}\` references external URL \`${url}\`.`, "External URLs in skill instructions can lead to link rot, phishing risk, or unauthorized telemetry when an agent follows them.", "Replace the external URL with a local reference, or document the link only in the plugin README where reviewers can see it.", { filePath: relativeFilePath, url }));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return findings;
|
|
273
|
+
}
|
|
236
274
|
async function collectPromptPoisoningScanFiles(rootPath, currentPath = rootPath) {
|
|
237
275
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
238
276
|
const filePaths = [];
|
|
@@ -270,6 +308,45 @@ async function auditPromptPoisoningSurface(rootPath) {
|
|
|
270
308
|
}
|
|
271
309
|
return findings;
|
|
272
310
|
}
|
|
311
|
+
async function collectChildProcessScanFiles(rootPath, currentPath = rootPath) {
|
|
312
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
313
|
+
const filePaths = [];
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
316
|
+
if (entry.isDirectory()) {
|
|
317
|
+
if (poisonScanSkippedDirectories.has(entry.name)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
filePaths.push(...(await collectChildProcessScanFiles(rootPath, entryPath)));
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!entry.isFile() || !childProcessScanExtensions.has(path.extname(entry.name).toLowerCase())) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const details = await stat(entryPath);
|
|
327
|
+
if (details.size <= 512 * 1024) {
|
|
328
|
+
filePaths.push(entryPath);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return filePaths;
|
|
332
|
+
}
|
|
333
|
+
export async function auditChildProcessSourceSurface(rootPath) {
|
|
334
|
+
const findings = [];
|
|
335
|
+
for (const filePath of await collectChildProcessScanFiles(rootPath)) {
|
|
336
|
+
const content = await readFile(filePath, "utf8");
|
|
337
|
+
if (!importsChildProcess(content)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const relativeFilePath = relativePackagePath(rootPath, filePath);
|
|
341
|
+
if (sourceUsesShellOption(content)) {
|
|
342
|
+
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 }));
|
|
343
|
+
}
|
|
344
|
+
if (sourceUsesShellCommand(content)) {
|
|
345
|
+
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 }));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return findings;
|
|
349
|
+
}
|
|
273
350
|
async function auditMcpCommandSurface(discoveredPackage) {
|
|
274
351
|
const { manifest, rootPath } = discoveredPackage;
|
|
275
352
|
if (!manifest.mcpServers) {
|
|
@@ -343,7 +420,9 @@ export async function buildSecurityAudit(targetPath) {
|
|
|
343
420
|
const findings = [
|
|
344
421
|
...validationSecurityFindings,
|
|
345
422
|
...(await auditMcpCommandSurface(discoveredPackage)),
|
|
346
|
-
...(await
|
|
423
|
+
...(await auditChildProcessSourceSurface(discoveredPackage.rootPath)),
|
|
424
|
+
...(await auditPromptPoisoningSurface(discoveredPackage.rootPath)),
|
|
425
|
+
...(await auditSkillExternalReferences(discoveredPackage.rootPath))
|
|
347
426
|
];
|
|
348
427
|
return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
|
|
349
428
|
}
|
package/package.json
CHANGED