codex-plugin-doctor 1.31.0 → 1.33.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.d.ts +1 -0
- package/dist/core/dep-audit.js +37 -0
- package/dist/core/watch-plugin.d.ts +4 -0
- package/dist/core/watch-plugin.js +84 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/policy/policy-packs.d.ts +2 -0
- package/dist/policy/policy-packs.js +9 -0
- package/dist/rules/rule-catalog.js +9 -0
- package/dist/run-cli.js +49 -10
- package/dist/security/security-audit.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -400,9 +400,9 @@ jobs:
|
|
|
400
400
|
runs-on: ubuntu-latest
|
|
401
401
|
steps:
|
|
402
402
|
- uses: actions/checkout@v5
|
|
403
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
403
|
+
- uses: Esquetta/CodexPluginDoctor@v1.33.0
|
|
404
404
|
with:
|
|
405
|
-
version: "1.
|
|
405
|
+
version: "1.33.0"
|
|
406
406
|
path: .
|
|
407
407
|
runtime: "true"
|
|
408
408
|
policy: codex-publish
|
package/dist/core/dep-audit.d.ts
CHANGED
|
@@ -15,3 +15,4 @@ export interface DepAuditReport {
|
|
|
15
15
|
export declare function buildDepAudit(targetPath: string): Promise<DepAuditReport>;
|
|
16
16
|
export declare function renderDepAudit(report: DepAuditReport): string;
|
|
17
17
|
export declare function renderDepAuditJson(report: DepAuditReport): string;
|
|
18
|
+
export declare function renderDepAuditSarif(report: DepAuditReport): string;
|
package/dist/core/dep-audit.js
CHANGED
|
@@ -171,3 +171,40 @@ export function renderDepAuditJson(report) {
|
|
|
171
171
|
audit: report.auditJson
|
|
172
172
|
}, null, 2);
|
|
173
173
|
}
|
|
174
|
+
export function renderDepAuditSarif(report) {
|
|
175
|
+
const rules = report.vulnerabilities.map((vuln) => ({
|
|
176
|
+
id: `dep-audit.${vuln.name}`,
|
|
177
|
+
name: vuln.name,
|
|
178
|
+
shortDescription: {
|
|
179
|
+
text: `${vuln.severity} severity vulnerability in ${vuln.name}${vuln.fixAvailable ? " (fix available)" : ""}`
|
|
180
|
+
},
|
|
181
|
+
help: {
|
|
182
|
+
text: vuln.via.length > 0 ? `Via: ${vuln.via.join(", ")}` : "No additional information."
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
const results = report.vulnerabilities.map((vuln, index) => ({
|
|
186
|
+
ruleId: `dep-audit.${vuln.name}`,
|
|
187
|
+
ruleIndex: index,
|
|
188
|
+
level: vuln.severity === "critical" || vuln.severity === "high" ? "error" : "warning",
|
|
189
|
+
message: {
|
|
190
|
+
text: `${vuln.severity}: ${vuln.name} ${vuln.fixAvailable ? "(fix available)" : "(no fix available)"}`
|
|
191
|
+
}
|
|
192
|
+
}));
|
|
193
|
+
const sarif = {
|
|
194
|
+
version: "2.1.0",
|
|
195
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
196
|
+
runs: [
|
|
197
|
+
{
|
|
198
|
+
tool: {
|
|
199
|
+
driver: {
|
|
200
|
+
name: "codex-plugin-doctor (dep-audit)",
|
|
201
|
+
informationUri: "https://github.com/Esquetta/CodexPluginDoctor",
|
|
202
|
+
rules
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
results
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
};
|
|
209
|
+
return JSON.stringify(sarif, null, 2);
|
|
210
|
+
}
|
|
@@ -4,10 +4,14 @@ export interface WatchPluginOptions {
|
|
|
4
4
|
runtime?: boolean;
|
|
5
5
|
jsonOutput?: boolean;
|
|
6
6
|
outputPath?: string | null;
|
|
7
|
+
maxIterations?: number;
|
|
8
|
+
failFast?: boolean;
|
|
9
|
+
accumulateJsonPath?: string | null;
|
|
7
10
|
}
|
|
8
11
|
export interface WatchPluginResult {
|
|
9
12
|
targetPath: string;
|
|
10
13
|
validations: number;
|
|
11
14
|
failures: number;
|
|
15
|
+
iterationsReached?: boolean;
|
|
12
16
|
}
|
|
13
17
|
export declare function watchPlugin(options: WatchPluginOptions): Promise<WatchPluginResult>;
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { validatePlugin } from "./validate-plugin.js";
|
|
5
|
+
const ignoredDirs = new Set([
|
|
6
|
+
"node_modules",
|
|
7
|
+
".git",
|
|
8
|
+
"dist",
|
|
9
|
+
"coverage",
|
|
10
|
+
".cache",
|
|
11
|
+
"build",
|
|
12
|
+
".turbo",
|
|
13
|
+
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
"__pycache__"
|
|
16
|
+
]);
|
|
17
|
+
function isIgnored(filename) {
|
|
18
|
+
for (const dir of ignoredDirs) {
|
|
19
|
+
if (filename.includes(dir) || filename.startsWith(`${dir}/`) || filename.startsWith(`${dir}\\`)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
4
25
|
function renderWatchValidation(result, iteration, jsonOutput) {
|
|
5
26
|
if (jsonOutput) {
|
|
6
27
|
const report = {
|
|
@@ -12,7 +33,7 @@ function renderWatchValidation(result, iteration, jsonOutput) {
|
|
|
12
33
|
findingsCount: result.findings.length,
|
|
13
34
|
findings: result.findings
|
|
14
35
|
};
|
|
15
|
-
return JSON.stringify(report
|
|
36
|
+
return JSON.stringify(report);
|
|
16
37
|
}
|
|
17
38
|
const icon = result.status === "pass" ? "PASS" : "FAIL";
|
|
18
39
|
return [
|
|
@@ -24,25 +45,40 @@ function renderWatchValidation(result, iteration, jsonOutput) {
|
|
|
24
45
|
].filter(Boolean).join("\n");
|
|
25
46
|
}
|
|
26
47
|
export async function watchPlugin(options) {
|
|
27
|
-
const { targetPath, debounceMs = 300, runtime = false, jsonOutput = false, outputPath = null } = options;
|
|
48
|
+
const { targetPath, debounceMs = 300, runtime = false, jsonOutput = false, outputPath = null, maxIterations = 0, failFast = false, accumulateJsonPath = null } = options;
|
|
28
49
|
const resolvedPath = path.resolve(targetPath);
|
|
29
50
|
let validations = 0;
|
|
30
51
|
let failures = 0;
|
|
31
52
|
let timer = null;
|
|
32
53
|
let pending = false;
|
|
33
54
|
const runValidation = async (iteration) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
try {
|
|
56
|
+
const result = await validatePlugin(resolvedPath, { runtime });
|
|
57
|
+
if (result.status !== "pass") {
|
|
58
|
+
failures += 1;
|
|
59
|
+
}
|
|
60
|
+
const output = renderWatchValidation(result, iteration, jsonOutput);
|
|
61
|
+
if (outputPath) {
|
|
62
|
+
await writeFile(outputPath, output, "utf8");
|
|
63
|
+
}
|
|
64
|
+
if (accumulateJsonPath && jsonOutput) {
|
|
65
|
+
await writeFile(accumulateJsonPath, `${output}\n`, { encoding: "utf8", flag: "a" });
|
|
66
|
+
}
|
|
67
|
+
if (!outputPath && !accumulateJsonPath) {
|
|
68
|
+
if (jsonOutput) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(JSON.parse(output), null, 2)}\n\n`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
process.stdout.write(`${output}\n\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
38
75
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await writeFile(outputPath, output, "utf8");
|
|
76
|
+
catch (error) {
|
|
77
|
+
process.stderr.write(`Watch validation error: ${error.message}\n`);
|
|
78
|
+
failures += 1;
|
|
43
79
|
}
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
finally {
|
|
81
|
+
validations += 1;
|
|
46
82
|
}
|
|
47
83
|
};
|
|
48
84
|
return new Promise((resolve, _reject) => {
|
|
@@ -60,10 +96,29 @@ export async function watchPlugin(options) {
|
|
|
60
96
|
timer = null;
|
|
61
97
|
iteration += 1;
|
|
62
98
|
await runValidation(iteration);
|
|
99
|
+
if (failFast && failures > 0) {
|
|
100
|
+
cleanup();
|
|
101
|
+
resolve({ targetPath: resolvedPath, validations, failures });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (maxIterations > 0 && validations >= maxIterations) {
|
|
105
|
+
cleanup();
|
|
106
|
+
resolve({ targetPath: resolvedPath, validations, failures, iterationsReached: true });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (maxIterations > 0) {
|
|
110
|
+
schedule();
|
|
111
|
+
}
|
|
63
112
|
}, debounceMs);
|
|
64
113
|
};
|
|
65
114
|
const watcher = watch(resolvedPath, { recursive: true }, (_eventType, filename) => {
|
|
66
|
-
if (!filename ||
|
|
115
|
+
if (!filename || isIgnored(filename)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (outputPath && filename === path.basename(outputPath)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (accumulateJsonPath && filename === path.basename(accumulateJsonPath)) {
|
|
67
122
|
return;
|
|
68
123
|
}
|
|
69
124
|
schedule();
|
|
@@ -71,17 +126,31 @@ export async function watchPlugin(options) {
|
|
|
71
126
|
watcher.on("error", (error) => {
|
|
72
127
|
process.stderr.write(`Watch error: ${error.message}\n`);
|
|
73
128
|
});
|
|
74
|
-
const
|
|
129
|
+
const cleanup = () => {
|
|
75
130
|
if (timer !== null) {
|
|
76
131
|
clearTimeout(timer);
|
|
77
132
|
}
|
|
78
133
|
watcher.close();
|
|
134
|
+
process.removeListener("SIGINT", handleExit);
|
|
135
|
+
process.removeListener("SIGTERM", handleExit);
|
|
136
|
+
process.removeListener("SIGHUP", handleExit);
|
|
137
|
+
};
|
|
138
|
+
const handleExit = () => {
|
|
139
|
+
cleanup();
|
|
79
140
|
resolve({ targetPath: resolvedPath, validations, failures });
|
|
80
141
|
};
|
|
81
142
|
process.on("SIGINT", handleExit);
|
|
82
143
|
process.on("SIGTERM", handleExit);
|
|
83
144
|
process.on("SIGHUP", handleExit);
|
|
84
|
-
|
|
145
|
+
const flags = [];
|
|
146
|
+
if (maxIterations > 0) {
|
|
147
|
+
flags.push(`max-iterations: ${maxIterations}`);
|
|
148
|
+
}
|
|
149
|
+
if (failFast) {
|
|
150
|
+
flags.push("fail-fast");
|
|
151
|
+
}
|
|
152
|
+
const flagsStr = flags.length > 0 ? `, ${flags.join(", ")}` : "";
|
|
153
|
+
process.stdout.write(`Watching ${resolvedPath} for changes (debounce: ${debounceMs}ms, runtime: ${runtime ? "enabled" : "disabled"}${flagsStr})...\nPress Ctrl+C to stop.\n\n`);
|
|
85
154
|
schedule();
|
|
86
155
|
});
|
|
87
156
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,9 +22,9 @@ export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctor
|
|
|
22
22
|
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
|
|
23
23
|
export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson, type BuildDoctorInspectorReportOptions, type DoctorInspectorReport } from "./core/inspector-bridge.js";
|
|
24
24
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
|
|
25
|
-
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
|
|
25
|
+
export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
|
|
26
26
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
|
|
27
27
|
export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
|
|
28
|
-
export { buildDepAudit, renderDepAudit, renderDepAuditJson, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
|
|
28
|
+
export { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
|
|
29
29
|
export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
|
|
30
30
|
export declare function runCheck(targetPath: string, options?: CheckOptions): Promise<CheckResult>;
|
package/dist/index.js
CHANGED
|
@@ -22,10 +22,10 @@ export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctor
|
|
|
22
22
|
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
|
|
23
23
|
export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
|
|
24
24
|
export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
|
|
25
|
-
export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
25
|
+
export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
26
26
|
export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
|
|
27
27
|
export { watchPlugin } from "./core/watch-plugin.js";
|
|
28
|
-
export { buildDepAudit, renderDepAudit, renderDepAuditJson } from "./core/dep-audit.js";
|
|
28
|
+
export { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
|
|
29
29
|
export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
|
|
30
30
|
export async function runCheck(targetPath, options = {}) {
|
|
31
31
|
return validatePlugin(targetPath, options);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DoctorConfig } from "../core/doctor-config.js";
|
|
2
|
+
import type { DepAuditReport } from "../core/dep-audit.js";
|
|
2
3
|
import type { SecurityAudit } from "../security/security-audit.js";
|
|
3
4
|
export declare const policyPackNames: readonly ["codex-publish", "mcp-strict", "security"];
|
|
4
5
|
export type PolicyPackName = (typeof policyPackNames)[number];
|
|
@@ -7,3 +8,4 @@ export declare function policyEnablesRuntime(policy: PolicyPackName | null): boo
|
|
|
7
8
|
export declare function policyFailsOnWarnings(policy: PolicyPackName | null): boolean;
|
|
8
9
|
export declare function applyPolicyToDoctorConfig(config: DoctorConfig, policy: PolicyPackName | null): DoctorConfig;
|
|
9
10
|
export declare function applyPolicyToSecurityAudit(audit: SecurityAudit, policy: PolicyPackName | null): SecurityAudit;
|
|
11
|
+
export declare function applyPolicyToDepAudit(report: DepAuditReport, policy: PolicyPackName | null): DepAuditReport;
|
|
@@ -31,3 +31,12 @@ export function applyPolicyToSecurityAudit(audit, policy) {
|
|
|
31
31
|
status: "fail"
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
export function applyPolicyToDepAudit(report, policy) {
|
|
35
|
+
if (!policyFailsOnWarnings(policy)) {
|
|
36
|
+
return report;
|
|
37
|
+
}
|
|
38
|
+
if (report.status === "warn") {
|
|
39
|
+
return { ...report, status: "fail" };
|
|
40
|
+
}
|
|
41
|
+
return report;
|
|
42
|
+
}
|
|
@@ -251,6 +251,15 @@ export const ruleCatalog = [
|
|
|
251
251
|
fix: "Use HTTPS for remote MCP servers; reserve HTTP for explicit localhost development endpoints.",
|
|
252
252
|
example: '{ "url": "https://example.com/mcp" }'
|
|
253
253
|
},
|
|
254
|
+
{
|
|
255
|
+
id: "plugin.security.mcp_binds_all_interfaces",
|
|
256
|
+
category: "security",
|
|
257
|
+
defaultSeverity: "warn",
|
|
258
|
+
summary: "An MCP server listens on `0.0.0.0` instead of `localhost`.",
|
|
259
|
+
why: "Servers that bind to all interfaces can accept connections from external hosts, which is rarely intended for local MCP development.",
|
|
260
|
+
fix: "Use `127.0.0.1` or `localhost` in server URLs and transport configuration unless external access is explicitly required.",
|
|
261
|
+
example: '{ "url": "http://127.0.0.1:3000/mcp" }'
|
|
262
|
+
},
|
|
254
263
|
{
|
|
255
264
|
id: "plugin.security.prompt_injection_text",
|
|
256
265
|
category: "security",
|
package/dist/run-cli.js
CHANGED
|
@@ -31,7 +31,7 @@ import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonRepo
|
|
|
31
31
|
import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
|
|
32
32
|
import { initCiWorkflow } from "./core/init-ci.js";
|
|
33
33
|
import { watchPlugin } from "./core/watch-plugin.js";
|
|
34
|
-
import { buildDepAudit, renderDepAudit, renderDepAuditJson } from "./core/dep-audit.js";
|
|
34
|
+
import { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
|
|
35
35
|
import { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
|
|
36
36
|
import { initPluginPackage, initPluginTemplates, isInitPluginTemplate } from "./core/init-plugin.js";
|
|
37
37
|
import { runCheck } from "./index.js";
|
|
@@ -48,7 +48,7 @@ import { renderSuppressionList, renderSuppressionListJson, renderSuppressionMuta
|
|
|
48
48
|
import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
|
|
49
49
|
import { renderSarifReport } from "./reporting/render-sarif-report.js";
|
|
50
50
|
import { renderTextReport } from "./reporting/render-text-report.js";
|
|
51
|
-
import { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
51
|
+
import { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
|
|
52
52
|
import { findRuleDefinition } from "./rules/rule-catalog.js";
|
|
53
53
|
import { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
|
|
54
54
|
import { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
|
|
@@ -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> [--json] [--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>]\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]\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] [--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 = [
|
|
@@ -1815,23 +1815,41 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1815
1815
|
: remainingArgs;
|
|
1816
1816
|
const runtime = watchFlags.includes("--runtime");
|
|
1817
1817
|
const jsonOutput = watchFlags.includes("--json");
|
|
1818
|
+
const failFast = watchFlags.includes("--fail-fast");
|
|
1818
1819
|
const outputIndex = watchFlags.indexOf("--output");
|
|
1819
1820
|
const outputPath = outputIndex === -1 ? null : watchFlags[outputIndex + 1];
|
|
1820
1821
|
const debounceIndex = watchFlags.indexOf("--debounce-ms");
|
|
1821
1822
|
const debounceRaw = debounceIndex === -1 ? null : watchFlags[debounceIndex + 1];
|
|
1822
1823
|
const debounceMs = debounceRaw ? Number(debounceRaw) || 300 : 300;
|
|
1824
|
+
const maxIterationsIndex = watchFlags.indexOf("--max-iterations");
|
|
1825
|
+
const maxIterationsRaw = maxIterationsIndex === -1 ? null : watchFlags[maxIterationsIndex + 1];
|
|
1826
|
+
const maxIterations = maxIterationsRaw ? Number(maxIterationsRaw) || 0 : 0;
|
|
1827
|
+
const accumulateIndex = watchFlags.indexOf("--accumulate-json");
|
|
1828
|
+
const accumulatePath = accumulateIndex === -1 ? null : watchFlags[accumulateIndex + 1];
|
|
1823
1829
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
1824
1830
|
io.writeStderr("Missing path after --output.");
|
|
1825
1831
|
return 2;
|
|
1826
1832
|
}
|
|
1833
|
+
if (maxIterationsIndex !== -1 && (!maxIterationsRaw || maxIterationsRaw.startsWith("--"))) {
|
|
1834
|
+
io.writeStderr("Missing number after --max-iterations.");
|
|
1835
|
+
return 2;
|
|
1836
|
+
}
|
|
1837
|
+
if (accumulateIndex !== -1 && (!accumulatePath || accumulatePath.startsWith("--"))) {
|
|
1838
|
+
io.writeStderr("Missing path after --accumulate-json.");
|
|
1839
|
+
return 2;
|
|
1840
|
+
}
|
|
1827
1841
|
const result = await watchPlugin({
|
|
1828
1842
|
targetPath,
|
|
1829
1843
|
debounceMs,
|
|
1830
1844
|
runtime,
|
|
1831
1845
|
jsonOutput,
|
|
1832
|
-
outputPath
|
|
1846
|
+
outputPath,
|
|
1847
|
+
maxIterations,
|
|
1848
|
+
failFast,
|
|
1849
|
+
accumulateJsonPath: accumulatePath
|
|
1833
1850
|
});
|
|
1834
|
-
|
|
1851
|
+
const reason = result.iterationsReached ? "max iterations reached" : "stopped";
|
|
1852
|
+
io.writeStdout(`\nStopped watching ${result.targetPath}: ${result.validations} validations, ${result.failures} failures (${reason}).`);
|
|
1835
1853
|
return result.failures > 0 ? 1 : 0;
|
|
1836
1854
|
}
|
|
1837
1855
|
if (command === "fix") {
|
|
@@ -1945,16 +1963,37 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1945
1963
|
? remainingArgs
|
|
1946
1964
|
: remainingArgs.slice(1);
|
|
1947
1965
|
const jsonOutput = depsFlags.includes("--json");
|
|
1966
|
+
const sarifOutput = depsFlags.includes("--sarif");
|
|
1948
1967
|
const outputIndex = depsFlags.indexOf("--output");
|
|
1949
1968
|
const outputPath = outputIndex === -1 ? null : depsFlags[outputIndex + 1];
|
|
1969
|
+
const policyIndex = depsFlags.indexOf("--policy");
|
|
1970
|
+
const policyName = policyIndex === -1 ? null : depsFlags[policyIndex + 1];
|
|
1971
|
+
const policy = parsePolicyPack(policyName);
|
|
1950
1972
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
1951
1973
|
io.writeStderr("Missing path after --output.");
|
|
1952
1974
|
return 2;
|
|
1953
1975
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1976
|
+
if (policyIndex !== -1 && (!policyName || policyName.startsWith("--"))) {
|
|
1977
|
+
io.writeStderr("Missing policy after --policy.");
|
|
1978
|
+
return 2;
|
|
1979
|
+
}
|
|
1980
|
+
if (policyIndex !== -1 && !policy) {
|
|
1981
|
+
io.writeStderr(`Unknown policy: ${policyName}. Supported policies: ${policyPackNames.join(", ")}.`);
|
|
1982
|
+
return 2;
|
|
1983
|
+
}
|
|
1984
|
+
if (jsonOutput && sarifOutput) {
|
|
1985
|
+
io.writeStderr("Use either --json or --sarif, not both.");
|
|
1986
|
+
return 2;
|
|
1987
|
+
}
|
|
1988
|
+
let report = await buildDepAudit(targetPath);
|
|
1989
|
+
if (policy) {
|
|
1990
|
+
report = applyPolicyToDepAudit(report, policy);
|
|
1991
|
+
}
|
|
1992
|
+
const renderedReport = sarifOutput
|
|
1993
|
+
? renderDepAuditSarif(report)
|
|
1994
|
+
: jsonOutput
|
|
1995
|
+
? renderDepAuditJson(report)
|
|
1996
|
+
: renderDepAudit(report);
|
|
1958
1997
|
if (outputPath) {
|
|
1959
1998
|
await writeFile(outputPath, renderedReport, "utf8");
|
|
1960
1999
|
}
|
|
@@ -1964,7 +2003,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1964
2003
|
const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
|
|
1965
2004
|
const installed = auditFlags.includes("--installed");
|
|
1966
2005
|
if (!installed) {
|
|
1967
|
-
io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--json] [--output <path>]");
|
|
2006
|
+
io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--json|--sarif] [--output <path>]");
|
|
1968
2007
|
return 2;
|
|
1969
2008
|
}
|
|
1970
2009
|
const installedIndex = auditFlags.indexOf("--installed");
|
|
@@ -227,6 +227,9 @@ export function auditMcpServerConfig(rootPath, parsedConfig, options = {}) {
|
|
|
227
227
|
if (typeof url === "string" && /^http:\/\//i.test(url)) {
|
|
228
228
|
findings.push(buildFinding("warn", "plugin.security.insecure_http_url", `The MCP server \`${serverName}\` uses an insecure HTTP URL.`, "Plain HTTP transports can expose MCP traffic on non-local networks and make endpoint identity harder to verify.", "Use HTTPS for remote MCP servers; reserve HTTP for explicit localhost development endpoints.", { serverName, configPath, url }));
|
|
229
229
|
}
|
|
230
|
+
if (typeof url === "string" && /\/\/0\.0\.0\.0[:/]/i.test(url)) {
|
|
231
|
+
findings.push(buildFinding("warn", "plugin.security.mcp_binds_all_interfaces", `The MCP server \`${serverName}\` URL binds to \`0.0.0.0\`.`, "Servers that listen on all interfaces can accept connections from external hosts, which is rarely intended for local MCP development.", "Use `127.0.0.1` or `localhost` instead of `0.0.0.0` unless external access is explicitly required.", { serverName, configPath, url }));
|
|
232
|
+
}
|
|
230
233
|
}
|
|
231
234
|
return findings;
|
|
232
235
|
}
|
package/package.json
CHANGED