codex-plugin-doctor 1.50.0 → 1.52.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/LICENSE +21 -21
- package/README.md +17 -6
- package/dist/core/bounded-http-client.d.ts +23 -0
- package/dist/core/bounded-http-client.js +240 -0
- package/dist/core/mcp-conformance.d.ts +2 -0
- package/dist/core/mcp-conformance.js +215 -0
- package/dist/core/output-contract.js +104 -5
- package/dist/core/release-check.d.ts +2 -0
- package/dist/core/release-check.js +2 -0
- package/dist/core/release-evidence.d.ts +2 -0
- package/dist/core/release-evidence.js +111 -7
- package/dist/core/remote-mcp-probe.d.ts +16 -0
- package/dist/core/remote-mcp-probe.js +226 -0
- package/dist/core/remote-network-policy.d.ts +24 -0
- package/dist/core/remote-network-policy.js +125 -0
- package/dist/core/remote-oauth-readiness.d.ts +12 -0
- package/dist/core/remote-oauth-readiness.js +295 -0
- package/dist/core/remote-url-policy.d.ts +7 -0
- package/dist/core/remote-url-policy.js +49 -0
- package/dist/core/runtime-plan.d.ts +3 -0
- package/dist/core/runtime-plan.js +36 -2
- package/dist/core/runtime-policy.js +5 -1
- package/dist/core/runtime-probe.d.ts +11 -2
- package/dist/core/runtime-probe.js +375 -70
- package/dist/core/runtime-transcript.js +13 -3
- package/dist/core/validate-plugin.js +32 -1
- package/dist/domain/types.d.ts +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/mcp/generic-mcp-doctor.d.ts +10 -2
- package/dist/mcp/generic-mcp-doctor.js +44 -13
- package/dist/reporting/render-markdown-report.js +44 -13
- package/dist/reporting/render-text-report.js +39 -22
- package/dist/rules/rule-catalog.js +253 -1
- package/dist/run-cli.js +137 -9
- package/dist/security/security-audit.js +34 -7
- package/examples/README.md +94 -94
- package/examples/codex-doctor-generic-mcp/.mcp.json +10 -10
- package/examples/codex-doctor-generic-mcp/server.js +1 -1
- package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-risky/.mcp.json +12 -12
- package/examples/codex-doctor-risky/mock-server.js +1 -1
- package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -8
- package/examples/codex-doctor-runtime/.mcp.json +9 -9
- package/examples/codex-doctor-runtime/mock-server.js +237 -237
- package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -7
- package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -7
- package/package.json +1 -1
|
@@ -161,6 +161,24 @@ export const ruleCatalog = [
|
|
|
161
161
|
fix: "Add `command` for stdio servers or `url` for remote servers.",
|
|
162
162
|
example: '{ "command": "node", "args": ["server.js"] }'
|
|
163
163
|
},
|
|
164
|
+
{
|
|
165
|
+
id: "mcp.server.transport.conflict",
|
|
166
|
+
category: "mcp",
|
|
167
|
+
defaultSeverity: "fail",
|
|
168
|
+
summary: "An MCP server defines both command and URL transports.",
|
|
169
|
+
why: "Clients cannot select a transport deterministically when a server defines both process and remote connection settings.",
|
|
170
|
+
fix: "Keep either command for stdio or url for remote MCP, but not both.",
|
|
171
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "plugin.mcp.server.transport.conflict",
|
|
175
|
+
category: "mcp",
|
|
176
|
+
defaultSeverity: "fail",
|
|
177
|
+
summary: "A bundled MCP server defines both command and URL transports.",
|
|
178
|
+
why: "Codex cannot select a transport deterministically when a bundled server defines both process and remote connection settings.",
|
|
179
|
+
fix: "Keep either command for stdio or url for remote MCP, but not both.",
|
|
180
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
181
|
+
},
|
|
164
182
|
{
|
|
165
183
|
id: "plugin.security.path_traversal",
|
|
166
184
|
category: "security",
|
|
@@ -245,12 +263,66 @@ export const ruleCatalog = [
|
|
|
245
263
|
{
|
|
246
264
|
id: "plugin.security.insecure_http_url",
|
|
247
265
|
category: "security",
|
|
248
|
-
defaultSeverity: "
|
|
266
|
+
defaultSeverity: "fail",
|
|
249
267
|
summary: "An MCP server uses a plain HTTP URL.",
|
|
250
268
|
why: "Plain HTTP can expose MCP traffic and does not verify endpoint identity on non-local networks.",
|
|
251
269
|
fix: "Use HTTPS for remote MCP servers; reserve HTTP for explicit localhost development endpoints.",
|
|
252
270
|
example: '{ "url": "https://example.com/mcp" }'
|
|
253
271
|
},
|
|
272
|
+
{
|
|
273
|
+
id: "plugin.security.remote_mcp_url.invalid",
|
|
274
|
+
category: "security",
|
|
275
|
+
defaultSeverity: "fail",
|
|
276
|
+
summary: "An MCP server URL is not an absolute HTTP or HTTPS URL.",
|
|
277
|
+
why: "Clients cannot reliably connect to malformed or relative remote MCP endpoints.",
|
|
278
|
+
fix: "Use an absolute HTTPS URL, or an explicit localhost HTTP development URL.",
|
|
279
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "plugin.security.remote_mcp_url.unsupported_scheme",
|
|
283
|
+
category: "security",
|
|
284
|
+
defaultSeverity: "fail",
|
|
285
|
+
summary: "An MCP server URL uses an unsupported scheme.",
|
|
286
|
+
why: "Remote MCP transport supports only HTTP and HTTPS endpoints.",
|
|
287
|
+
fix: "Use an HTTPS URL, or an explicit localhost HTTP development URL.",
|
|
288
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: "plugin.security.remote_mcp_url.credentials",
|
|
292
|
+
category: "security",
|
|
293
|
+
defaultSeverity: "fail",
|
|
294
|
+
summary: "An MCP server URL embeds credentials.",
|
|
295
|
+
why: "URL credentials can leak through configuration, logs, reports, and package artifacts.",
|
|
296
|
+
fix: "Remove URL credentials and configure authentication outside the endpoint URL.",
|
|
297
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: "plugin.security.remote_mcp_url.query",
|
|
301
|
+
category: "security",
|
|
302
|
+
defaultSeverity: "fail",
|
|
303
|
+
summary: "An MCP server URL contains a query string.",
|
|
304
|
+
why: "Query strings can carry secrets and make endpoint configuration ambiguous.",
|
|
305
|
+
fix: "Remove the query string from the MCP endpoint URL.",
|
|
306
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: "plugin.security.remote_mcp_url.fragment",
|
|
310
|
+
category: "security",
|
|
311
|
+
defaultSeverity: "fail",
|
|
312
|
+
summary: "An MCP server URL contains a fragment.",
|
|
313
|
+
why: "Fragments are not sent to servers and can hide misleading endpoint configuration.",
|
|
314
|
+
fix: "Remove the fragment from the MCP endpoint URL.",
|
|
315
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: "plugin.security.remote_mcp_url.ip_literal",
|
|
319
|
+
category: "security",
|
|
320
|
+
defaultSeverity: "fail",
|
|
321
|
+
summary: "An MCP server URL uses a numeric IP literal.",
|
|
322
|
+
why: "Numeric IP endpoints bypass hostname-based endpoint review and are not an accepted remote MCP shape.",
|
|
323
|
+
fix: "Use a reviewed hostname; use localhost only for local development.",
|
|
324
|
+
example: '{ "url": "https://example.com/mcp" }'
|
|
325
|
+
},
|
|
254
326
|
{
|
|
255
327
|
id: "plugin.security.mcp_binds_all_interfaces",
|
|
256
328
|
category: "security",
|
|
@@ -278,6 +350,78 @@ export const ruleCatalog = [
|
|
|
278
350
|
fix: "Remove hidden override or exfiltration instructions and keep descriptions scoped to legitimate behavior.",
|
|
279
351
|
example: "Keep SKILL.md, prompt, resource, and tool descriptions direct and user-facing."
|
|
280
352
|
},
|
|
353
|
+
{
|
|
354
|
+
id: "mcp.conformance.protocol.unknown_newer",
|
|
355
|
+
category: "mcp",
|
|
356
|
+
defaultSeverity: "warn",
|
|
357
|
+
summary: "The server advertises a newer MCP protocol version.",
|
|
358
|
+
why: "The validator applies the 2025-11-25 structural baseline, which may not cover newer protocol requirements.",
|
|
359
|
+
fix: "Confirm the server's newer protocol changes remain compatible with the 2025-11-25 MCP contract.",
|
|
360
|
+
example: '{ "protocolVersion": "2026-01-01" }'
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: "mcp.conformance.tasks.capability_invalid",
|
|
364
|
+
category: "mcp",
|
|
365
|
+
defaultSeverity: "fail",
|
|
366
|
+
summary: "A Tasks capability declaration is malformed.",
|
|
367
|
+
why: "Codex cannot safely determine whether the server supports task-aware tool calls.",
|
|
368
|
+
fix: "Return an object for the affected Tasks capability field, or omit the capability until it is implemented.",
|
|
369
|
+
example: '{ "capabilities": { "tasks": { "requests": { "tools": { "call": {} } } } } }'
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: "mcp.conformance.tasks.task_support_invalid",
|
|
373
|
+
category: "mcp",
|
|
374
|
+
defaultSeverity: "fail",
|
|
375
|
+
summary: "A tool declares invalid task support.",
|
|
376
|
+
why: "Codex cannot safely interpret the tool's task support contract.",
|
|
377
|
+
fix: "Set execution.taskSupport to required, optional, or forbidden.",
|
|
378
|
+
example: '{ "execution": { "taskSupport": "optional" } }'
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "mcp.conformance.tasks.capability_mismatch",
|
|
382
|
+
category: "mcp",
|
|
383
|
+
defaultSeverity: "fail",
|
|
384
|
+
summary: "A tool's task support does not match server capability.",
|
|
385
|
+
why: "Codex may attempt task-aware tool calls that the server did not advertise as supported.",
|
|
386
|
+
fix: "Advertise capabilities.tasks.requests.tools.call as an object, or set execution.taskSupport to forbidden.",
|
|
387
|
+
example: '{ "capabilities": { "tasks": { "requests": { "tools": { "call": {} } } } }, "execution": { "taskSupport": "required" } }'
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: "mcp.conformance.tasks_list.timeout",
|
|
391
|
+
category: "mcp",
|
|
392
|
+
defaultSeverity: "fail",
|
|
393
|
+
summary: "The server did not complete tasks/list in time.",
|
|
394
|
+
why: "Codex cannot rely on task discovery when tasks/list cannot complete with a valid response.",
|
|
395
|
+
fix: "Reduce tasks/list latency and verify pagination completes.",
|
|
396
|
+
example: "Return a valid tasks/list response before the configured runtime timeout."
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: "mcp.conformance.tasks_list.invalid",
|
|
400
|
+
category: "mcp",
|
|
401
|
+
defaultSeverity: "fail",
|
|
402
|
+
summary: "The server returned an invalid tasks/list response.",
|
|
403
|
+
why: "Codex cannot rely on task discovery when tasks/list cannot complete with a valid response.",
|
|
404
|
+
fix: "Return a valid tasks/list response with well-formed task items and pagination.",
|
|
405
|
+
example: '{ "tasks": [] }'
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "mcp.conformance.schema.dialect_invalid",
|
|
409
|
+
category: "mcp",
|
|
410
|
+
defaultSeverity: "fail",
|
|
411
|
+
summary: "A tool schema declares an invalid dialect.",
|
|
412
|
+
why: "Codex cannot reliably select a schema dialect for this tool.",
|
|
413
|
+
fix: "Omit $schema or provide an absolute URI in the affected tool schema.",
|
|
414
|
+
example: '{ "inputSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema" } }'
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "mcp.conformance.schema.dialect_unsupported",
|
|
418
|
+
category: "mcp",
|
|
419
|
+
defaultSeverity: "warn",
|
|
420
|
+
summary: "A tool schema uses a non-canonical dialect.",
|
|
421
|
+
why: "The schema may be valid, but its dialect is outside the validator's latest compatibility baseline.",
|
|
422
|
+
fix: "Use https://json-schema.org/draft/2020-12/schema or omit $schema.",
|
|
423
|
+
example: '{ "inputSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema" } }'
|
|
424
|
+
},
|
|
281
425
|
{
|
|
282
426
|
id: "plugin.runtime.exited_early",
|
|
283
427
|
category: "runtime",
|
|
@@ -304,6 +448,114 @@ export const ruleCatalog = [
|
|
|
304
448
|
why: "MCP stdio transport requires newline-delimited JSON-RPC messages on stdout.",
|
|
305
449
|
fix: "Send logs to stderr and reserve stdout for JSON-RPC protocol messages only.",
|
|
306
450
|
example: "Use `console.error` for diagnostics in Node stdio servers."
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "plugin.runtime.remote.network_not_approved",
|
|
454
|
+
category: "runtime",
|
|
455
|
+
defaultSeverity: "fail",
|
|
456
|
+
summary: "Remote MCP probing was not explicitly approved.",
|
|
457
|
+
why: "Remote initialization creates outbound network traffic.",
|
|
458
|
+
fix: "Review the plan, then use --runtime --allow-network; add --allow-local-network only for localhost HTTP.",
|
|
459
|
+
example: "codex-plugin-doctor mcp . --runtime --allow-network"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "plugin.runtime.remote.url.invalid",
|
|
463
|
+
category: "runtime",
|
|
464
|
+
defaultSeverity: "fail",
|
|
465
|
+
summary: "A remote MCP endpoint URL is unsafe or unsupported.",
|
|
466
|
+
why: "Credentials, queries, fragments, IP literals, and unsupported schemes bypass the remote probe boundary.",
|
|
467
|
+
fix: "Use an absolute HTTPS URL without credentials, query parameters, fragments, or IP literals.",
|
|
468
|
+
example: "https://mcp.example/mcp"
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: "plugin.runtime.remote.transport.timeout",
|
|
472
|
+
category: "runtime",
|
|
473
|
+
defaultSeverity: "fail",
|
|
474
|
+
summary: "The remote MCP initialize request timed out.",
|
|
475
|
+
why: "A bounded probe cannot safely negotiate an unavailable endpoint.",
|
|
476
|
+
fix: "Make the endpoint reachable and complete initialize within the configured timeout.",
|
|
477
|
+
example: "Return a valid initialize response promptly."
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "plugin.runtime.remote.transport.response_too_large",
|
|
481
|
+
category: "runtime",
|
|
482
|
+
defaultSeverity: "fail",
|
|
483
|
+
summary: "The remote MCP response exceeded the bounded probe limit.",
|
|
484
|
+
why: "Unbounded remote responses can exhaust local resources.",
|
|
485
|
+
fix: "Return a compact initialize response and keep discovery metadata bounded.",
|
|
486
|
+
example: "Return only the MCP initialize result."
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: "plugin.runtime.remote.transport.failed",
|
|
490
|
+
category: "runtime",
|
|
491
|
+
defaultSeverity: "fail",
|
|
492
|
+
summary: "The remote MCP transport request failed.",
|
|
493
|
+
why: "Protocol negotiation cannot proceed without a safe bounded connection.",
|
|
494
|
+
fix: "Verify endpoint reachability, TLS, and remote network policy eligibility.",
|
|
495
|
+
example: "Use a public HTTPS endpoint or explicitly approved localhost HTTP."
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: "plugin.runtime.remote.http_status.invalid",
|
|
499
|
+
category: "runtime",
|
|
500
|
+
defaultSeverity: "fail",
|
|
501
|
+
summary: "The remote MCP initialize response used an unexpected HTTP status.",
|
|
502
|
+
why: "Streamable HTTP initialization requires a successful response before negotiation.",
|
|
503
|
+
fix: "Return HTTP 200 for initialize or publish valid OAuth discovery metadata for protected endpoints.",
|
|
504
|
+
example: "HTTP/1.1 200 OK"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: "plugin.runtime.remote.content_type.invalid",
|
|
508
|
+
category: "runtime",
|
|
509
|
+
defaultSeverity: "fail",
|
|
510
|
+
summary: "The remote MCP initialize response used an unsupported content type.",
|
|
511
|
+
why: "The probe can only validate JSON or Server-Sent Events protocol responses.",
|
|
512
|
+
fix: "Return application/json or text/event-stream with a JSON-RPC response.",
|
|
513
|
+
example: "Content-Type: application/json"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "plugin.runtime.remote.session.invalid",
|
|
517
|
+
category: "runtime",
|
|
518
|
+
defaultSeverity: "fail",
|
|
519
|
+
summary: "The remote MCP session header is invalid.",
|
|
520
|
+
why: "An invalid session identifier cannot be safely replayed on the initialized notification.",
|
|
521
|
+
fix: "Return MCP-Session-Id only as visible ASCII characters.",
|
|
522
|
+
example: "MCP-Session-Id: session-123"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: "plugin.runtime.remote.initialize.invalid",
|
|
526
|
+
category: "runtime",
|
|
527
|
+
defaultSeverity: "fail",
|
|
528
|
+
summary: "The remote MCP initialize JSON-RPC result is invalid.",
|
|
529
|
+
why: "Invalid negotiation results leave capabilities and protocol version unknown.",
|
|
530
|
+
fix: "Return a JSON-RPC 2.0 initialize result for protocol version 2025-11-25.",
|
|
531
|
+
example: '{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25" } }'
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: "plugin.runtime.remote.initialized.failed",
|
|
535
|
+
category: "runtime",
|
|
536
|
+
defaultSeverity: "fail",
|
|
537
|
+
summary: "The remote MCP server did not acknowledge notifications/initialized.",
|
|
538
|
+
why: "The session may not be ready for subsequent protocol traffic.",
|
|
539
|
+
fix: "Accept a successful initialized notification at the configured MCP endpoint.",
|
|
540
|
+
example: "Return HTTP 204 to notifications/initialized."
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
id: "plugin.runtime.remote.authorization.metadata.invalid",
|
|
544
|
+
category: "runtime",
|
|
545
|
+
defaultSeverity: "fail",
|
|
546
|
+
summary: "Remote OAuth discovery metadata is invalid.",
|
|
547
|
+
why: "Protected MCP endpoints cannot be assessed safely without valid authorization metadata.",
|
|
548
|
+
fix: "Publish valid HTTPS protected-resource and authorization-server metadata.",
|
|
549
|
+
example: "https://mcp.example/.well-known/oauth-protected-resource"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: "plugin.runtime.remote.authorization.metadata.unavailable",
|
|
553
|
+
category: "runtime",
|
|
554
|
+
defaultSeverity: "fail",
|
|
555
|
+
summary: "Remote OAuth discovery metadata is unavailable.",
|
|
556
|
+
why: "Authorization readiness cannot be confirmed without bounded metadata responses.",
|
|
557
|
+
fix: "Make protected-resource and authorization-server metadata available over HTTPS.",
|
|
558
|
+
example: "Return application/json discovery metadata."
|
|
307
559
|
}
|
|
308
560
|
];
|
|
309
561
|
export function findRuleDefinition(id) {
|
package/dist/run-cli.js
CHANGED
|
@@ -99,8 +99,41 @@ function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
|
|
|
99
99
|
}
|
|
100
100
|
return value;
|
|
101
101
|
}
|
|
102
|
+
function parseRemoteNetworkFlags(flags, runtime) {
|
|
103
|
+
let allowNetwork = false;
|
|
104
|
+
let allowLocalNetwork = false;
|
|
105
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
106
|
+
const flag = flags[index];
|
|
107
|
+
if (flag === "--allow-network") {
|
|
108
|
+
if (allowNetwork)
|
|
109
|
+
return new CliUsageError("Duplicate runtime network flag: --allow-network.");
|
|
110
|
+
if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
|
|
111
|
+
return new CliUsageError("--allow-network does not accept a value.");
|
|
112
|
+
}
|
|
113
|
+
allowNetwork = true;
|
|
114
|
+
}
|
|
115
|
+
else if (flag === "--allow-local-network") {
|
|
116
|
+
if (allowLocalNetwork)
|
|
117
|
+
return new CliUsageError("Duplicate runtime network flag: --allow-local-network.");
|
|
118
|
+
if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
|
|
119
|
+
return new CliUsageError("--allow-local-network does not accept a value.");
|
|
120
|
+
}
|
|
121
|
+
allowLocalNetwork = true;
|
|
122
|
+
}
|
|
123
|
+
else if (flag?.startsWith("--allow-network=") || flag?.startsWith("--allow-local-network=")) {
|
|
124
|
+
return new CliUsageError(`${flag.split("=", 1)[0]} does not accept a value.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if ((allowNetwork || allowLocalNetwork) && !runtime) {
|
|
128
|
+
return new CliUsageError("--allow-network requires --runtime.");
|
|
129
|
+
}
|
|
130
|
+
if (allowLocalNetwork && !allowNetwork) {
|
|
131
|
+
return new CliUsageError("--allow-local-network requires --allow-network.");
|
|
132
|
+
}
|
|
133
|
+
return { allowNetwork, allowLocalNetwork };
|
|
134
|
+
}
|
|
102
135
|
function printUsage(io) {
|
|
103
|
-
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] [--sandbox docker] [--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 [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--
|
|
136
|
+
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 [--allow-network [--allow-local-network]]] [--sandbox docker] [--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> [--runtime [--allow-network [--allow-local-network]]] [--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 [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|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 completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\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");
|
|
104
137
|
io.writeStderr("Corpus quality regression: codex-plugin-doctor doctor corpus metrics diff --before <metrics.json> --after <metrics.json> [--fail-on-regression] [--json|--markdown] [--output <path>]");
|
|
105
138
|
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]");
|
|
106
139
|
io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
|
|
@@ -647,17 +680,68 @@ async function executeSuppressCommand(command, io, options = {}) {
|
|
|
647
680
|
}
|
|
648
681
|
function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
|
|
649
682
|
if (!commandTarget || commandTarget.startsWith("--")) {
|
|
650
|
-
return "Missing target path. Usage: codex-plugin-doctor mcp <path> [--json] [--output <path>]";
|
|
683
|
+
return "Missing target path. Usage: codex-plugin-doctor mcp <path> [--runtime] [--json] [--output <path>]";
|
|
651
684
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
685
|
+
let jsonOutput = false;
|
|
686
|
+
let outputPath = null;
|
|
687
|
+
let runtime = false;
|
|
688
|
+
let allowNetwork = false;
|
|
689
|
+
let allowLocalNetwork = false;
|
|
690
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
691
|
+
const flag = flags[index];
|
|
692
|
+
if (flag === "--runtime") {
|
|
693
|
+
if (runtime) {
|
|
694
|
+
return "Duplicate MCP flag: --runtime.";
|
|
695
|
+
}
|
|
696
|
+
runtime = true;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (flag === "--json") {
|
|
700
|
+
if (jsonOutput) {
|
|
701
|
+
return "Duplicate MCP flag: --json.";
|
|
702
|
+
}
|
|
703
|
+
jsonOutput = true;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (flag === "--allow-network") {
|
|
707
|
+
if (allowNetwork)
|
|
708
|
+
return "Duplicate MCP flag: --allow-network.";
|
|
709
|
+
allowNetwork = true;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (flag === "--allow-local-network") {
|
|
713
|
+
if (allowLocalNetwork)
|
|
714
|
+
return "Duplicate MCP flag: --allow-local-network.";
|
|
715
|
+
allowLocalNetwork = true;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (flag === "--output") {
|
|
719
|
+
if (outputPath !== null) {
|
|
720
|
+
return "Duplicate MCP flag: --output.";
|
|
721
|
+
}
|
|
722
|
+
const value = flags[index + 1];
|
|
723
|
+
if (!value || value.startsWith("--")) {
|
|
724
|
+
return "Missing path after --output.";
|
|
725
|
+
}
|
|
726
|
+
outputPath = value;
|
|
727
|
+
index += 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
return flag.startsWith("--")
|
|
731
|
+
? `Unknown MCP flag: ${flag}.`
|
|
732
|
+
: `Unexpected MCP argument: ${flag}.`;
|
|
733
|
+
}
|
|
734
|
+
const remoteNetwork = parseRemoteNetworkFlags(flags, runtime);
|
|
735
|
+
if (remoteNetwork instanceof CliUsageError) {
|
|
736
|
+
return remoteNetwork.message;
|
|
656
737
|
}
|
|
657
738
|
return {
|
|
658
739
|
targetPath: commandTarget,
|
|
659
|
-
jsonOutput
|
|
660
|
-
outputPath
|
|
740
|
+
jsonOutput,
|
|
741
|
+
outputPath,
|
|
742
|
+
runtime,
|
|
743
|
+
allowNetwork,
|
|
744
|
+
allowLocalNetwork
|
|
661
745
|
};
|
|
662
746
|
}
|
|
663
747
|
function parsePerformanceThresholds(flags) {
|
|
@@ -944,6 +1028,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
944
1028
|
const report = await buildGenericMcpDoctor(parsedMcpArgs.targetPath, {
|
|
945
1029
|
env: terminalContext.env,
|
|
946
1030
|
platform: terminalContext.platform
|
|
1031
|
+
}, {
|
|
1032
|
+
runtime: parsedMcpArgs.runtime,
|
|
1033
|
+
allowNetwork: parsedMcpArgs.allowNetwork,
|
|
1034
|
+
allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
|
|
947
1035
|
});
|
|
948
1036
|
const renderedReport = parsedMcpArgs.jsonOutput
|
|
949
1037
|
? renderGenericMcpDoctorJson(report)
|
|
@@ -1185,7 +1273,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1185
1273
|
? null
|
|
1186
1274
|
: assetFlags[runtimeApprovalDigestIndex + 1];
|
|
1187
1275
|
const runtime = assetFlags.includes("--runtime");
|
|
1276
|
+
const remoteNetwork = parseRemoteNetworkFlags(assetFlags, runtime);
|
|
1188
1277
|
let runtimeSandbox;
|
|
1278
|
+
if (remoteNetwork instanceof CliUsageError) {
|
|
1279
|
+
io.writeStderr(remoteNetwork.message);
|
|
1280
|
+
return 2;
|
|
1281
|
+
}
|
|
1189
1282
|
try {
|
|
1190
1283
|
runtimeSandbox = parseRuntimeSandbox(assetFlags);
|
|
1191
1284
|
}
|
|
@@ -1249,6 +1342,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1249
1342
|
requireRuntimeApproval,
|
|
1250
1343
|
runtimeApprovalDigest,
|
|
1251
1344
|
runtime,
|
|
1345
|
+
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
1346
|
+
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
1252
1347
|
...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
|
|
1253
1348
|
environment: {
|
|
1254
1349
|
env: terminalContext.env,
|
|
@@ -1257,6 +1352,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1257
1352
|
runCheck: options.runCheckImpl ?? runCheck,
|
|
1258
1353
|
performanceThresholds: parsedThresholds.thresholds
|
|
1259
1354
|
});
|
|
1355
|
+
if (!evidence.releaseReady) {
|
|
1356
|
+
io.writeStdout(jsonOutput ? renderDoctorReleaseEvidenceJson(evidence) : renderDoctorReleaseEvidence(evidence));
|
|
1357
|
+
return evidence.exitCode;
|
|
1358
|
+
}
|
|
1260
1359
|
await writeFile(resolvedOutputPath, renderDoctorReleaseEvidenceJson(evidence), "utf8");
|
|
1261
1360
|
let uploaded = false;
|
|
1262
1361
|
const uploadArgs = ["release", "upload", tag, resolvedOutputPath, "--clobber"];
|
|
@@ -1354,7 +1453,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1354
1453
|
? null
|
|
1355
1454
|
: evidenceFlags[runtimeApprovalDigestIndex + 1];
|
|
1356
1455
|
const runtime = evidenceFlags.includes("--runtime");
|
|
1456
|
+
const remoteNetwork = parseRemoteNetworkFlags(evidenceFlags, runtime);
|
|
1357
1457
|
let runtimeSandbox;
|
|
1458
|
+
if (remoteNetwork instanceof CliUsageError) {
|
|
1459
|
+
io.writeStderr(remoteNetwork.message);
|
|
1460
|
+
return 2;
|
|
1461
|
+
}
|
|
1358
1462
|
try {
|
|
1359
1463
|
runtimeSandbox = parseRuntimeSandbox(evidenceFlags);
|
|
1360
1464
|
}
|
|
@@ -1401,6 +1505,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1401
1505
|
requireRuntimeApproval,
|
|
1402
1506
|
runtimeApprovalDigest,
|
|
1403
1507
|
runtime,
|
|
1508
|
+
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
1509
|
+
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
1404
1510
|
...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
|
|
1405
1511
|
environment: {
|
|
1406
1512
|
env: terminalContext.env,
|
|
@@ -2223,6 +2329,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2223
2329
|
const report = await buildGenericMcpDoctor(parsedMcpArgs.targetPath, {
|
|
2224
2330
|
env: terminalContext.env,
|
|
2225
2331
|
platform: terminalContext.platform
|
|
2332
|
+
}, {
|
|
2333
|
+
runtime: parsedMcpArgs.runtime,
|
|
2334
|
+
allowNetwork: parsedMcpArgs.allowNetwork,
|
|
2335
|
+
allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
|
|
2226
2336
|
});
|
|
2227
2337
|
const renderedReport = parsedMcpArgs.jsonOutput
|
|
2228
2338
|
? renderGenericMcpDoctorJson(report)
|
|
@@ -2499,7 +2609,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2499
2609
|
: remainingArgs.slice(1);
|
|
2500
2610
|
const jsonOutput = releaseFlags.includes("--json");
|
|
2501
2611
|
const runtimeProbeEnabled = releaseFlags.includes("--runtime");
|
|
2612
|
+
const remoteNetwork = parseRemoteNetworkFlags(releaseFlags, runtimeProbeEnabled);
|
|
2502
2613
|
let runtimeSandbox;
|
|
2614
|
+
if (remoteNetwork instanceof CliUsageError) {
|
|
2615
|
+
io.writeStderr(remoteNetwork.message);
|
|
2616
|
+
return 2;
|
|
2617
|
+
}
|
|
2503
2618
|
try {
|
|
2504
2619
|
runtimeSandbox = parseRuntimeSandbox(releaseFlags);
|
|
2505
2620
|
}
|
|
@@ -2536,6 +2651,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2536
2651
|
env: terminalContext.env,
|
|
2537
2652
|
platform: terminalContext.platform,
|
|
2538
2653
|
runtime: runtimeProbeEnabled,
|
|
2654
|
+
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2655
|
+
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
2539
2656
|
...(runtimeSandbox ? { runtimeSandbox } : {}),
|
|
2540
2657
|
runCheck: options.runCheckImpl
|
|
2541
2658
|
});
|
|
@@ -2682,6 +2799,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2682
2799
|
const effectiveRuntimeProbeEnabled = runtimeProbeEnabled ||
|
|
2683
2800
|
checkProfile === "publish" ||
|
|
2684
2801
|
policyEnablesRuntime(policy);
|
|
2802
|
+
const remoteNetwork = parseRemoteNetworkFlags(normalizedFlags, effectiveRuntimeProbeEnabled);
|
|
2803
|
+
if (remoteNetwork instanceof CliUsageError) {
|
|
2804
|
+
io.writeStderr(remoteNetwork.message);
|
|
2805
|
+
return 2;
|
|
2806
|
+
}
|
|
2685
2807
|
if (requireRuntimeApproval && !effectiveRuntimeProbeEnabled) {
|
|
2686
2808
|
io.writeStderr("Runtime approval requires runtime probing. Add --runtime, --profile publish, or a runtime-enabled policy.");
|
|
2687
2809
|
return 2;
|
|
@@ -2743,6 +2865,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2743
2865
|
plugin,
|
|
2744
2866
|
result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
|
|
2745
2867
|
runtime: effectiveRuntimeProbeEnabled,
|
|
2868
|
+
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2869
|
+
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
2746
2870
|
runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
|
|
2747
2871
|
? (line) => io.writeStderr(line)
|
|
2748
2872
|
: undefined
|
|
@@ -2812,7 +2936,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2812
2936
|
continue;
|
|
2813
2937
|
}
|
|
2814
2938
|
const pluginResult = applyDoctorConfig(await runCheckImpl(pluginRoot, {
|
|
2815
|
-
runtime: effectiveRuntimeProbeEnabled
|
|
2939
|
+
runtime: effectiveRuntimeProbeEnabled,
|
|
2940
|
+
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2941
|
+
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {})
|
|
2816
2942
|
}), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(pluginRoot, configPath), checkProfile), policy));
|
|
2817
2943
|
io.writeStdout(`\n${pluginResult.targetPath}: ${pluginResult.status.toUpperCase()}`);
|
|
2818
2944
|
validatedCount += 1;
|
|
@@ -2834,6 +2960,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2834
2960
|
const doctorConfig = applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile), policy);
|
|
2835
2961
|
const configuredResult = applyDoctorConfig(await runCheckImpl(targetPath, {
|
|
2836
2962
|
runtime: effectiveRuntimeProbeEnabled,
|
|
2963
|
+
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2964
|
+
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
2837
2965
|
...(runtimeSandbox ? { runtimeSandbox } : {}),
|
|
2838
2966
|
...(effectiveRuntimeProbeEnabled && verboseRuntime
|
|
2839
2967
|
? { runtimeTranscript: (line) => io.writeStderr(line) }
|
|
@@ -2,6 +2,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { discoverPackage } from "../core/discover-package.js";
|
|
4
4
|
import { readJsonFile } from "../core/read-json-file.js";
|
|
5
|
+
import { inspectRemoteMcpUrl } from "../core/remote-url-policy.js";
|
|
5
6
|
import { validatePlugin } from "../core/validate-plugin.js";
|
|
6
7
|
import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
7
8
|
import { formatFindingEvidenceLine } from "../reporting/format-finding-evidence.js";
|
|
@@ -18,6 +19,16 @@ function buildFinding(severity, id, message, impact, suggestedFix, evidence) {
|
|
|
18
19
|
function isPlainObject(value) {
|
|
19
20
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
21
|
}
|
|
22
|
+
function remoteUrlIssueFindingId(issue) {
|
|
23
|
+
return issue === "insecure_non_loopback"
|
|
24
|
+
? "plugin.security.insecure_http_url"
|
|
25
|
+
: `plugin.security.remote_mcp_url.${issue}`;
|
|
26
|
+
}
|
|
27
|
+
function remoteUrlIssueMessage(issue) {
|
|
28
|
+
return issue === "insecure_non_loopback"
|
|
29
|
+
? "uses an insecure public HTTP URL"
|
|
30
|
+
: `uses a remote MCP URL with ${issue.replaceAll("_", " ")}`;
|
|
31
|
+
}
|
|
21
32
|
function isPathWithinRoot(rootPath, candidatePath) {
|
|
22
33
|
const relativePath = path.relative(rootPath, candidatePath);
|
|
23
34
|
return (relativePath === "" ||
|
|
@@ -245,19 +256,33 @@ export function auditMcpServerConfig(rootPath, parsedConfig, options = {}) {
|
|
|
245
256
|
}));
|
|
246
257
|
}
|
|
247
258
|
}
|
|
248
|
-
if (typeof url === "string"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
259
|
+
if (typeof url === "string") {
|
|
260
|
+
const inspection = inspectRemoteMcpUrl(url);
|
|
261
|
+
for (const issue of inspection.issues) {
|
|
262
|
+
findings.push(buildFinding("fail", remoteUrlIssueFindingId(issue), `The MCP server \`${serverName}\` ${remoteUrlIssueMessage(issue)}.`, "Unsafe or ambiguous remote transport configuration can expose credentials or prevent reliable MCP connectivity.", "Use an absolute HTTPS URL without credentials, query parameters, fragments, or numeric IP literals; HTTP is only supported for localhost development.", {
|
|
263
|
+
serverName,
|
|
264
|
+
configPath,
|
|
265
|
+
url: inspection.sanitizedUrl
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
if (inspection.parsedUrl?.hostname === "0.0.0.0") {
|
|
269
|
+
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.", {
|
|
270
|
+
serverName,
|
|
271
|
+
configPath,
|
|
272
|
+
url: inspection.sanitizedUrl
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
253
275
|
}
|
|
254
276
|
}
|
|
255
277
|
return findings;
|
|
256
278
|
}
|
|
257
279
|
const externalUrlPattern = /https?:\/\/[^\s`"'<>)]+/gi;
|
|
258
|
-
async function auditSkillExternalReferences(rootPath) {
|
|
280
|
+
async function auditSkillExternalReferences(rootPath, mcpConfigPath) {
|
|
259
281
|
const findings = [];
|
|
260
282
|
for (const filePath of await collectPromptPoisoningScanFiles(rootPath)) {
|
|
283
|
+
if (mcpConfigPath !== null && path.resolve(filePath) === mcpConfigPath) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
261
286
|
const content = await readFile(filePath, "utf8");
|
|
262
287
|
const matches = content.match(externalUrlPattern);
|
|
263
288
|
if (!matches) {
|
|
@@ -422,7 +447,9 @@ export async function buildSecurityAudit(targetPath) {
|
|
|
422
447
|
...(await auditMcpCommandSurface(discoveredPackage)),
|
|
423
448
|
...(await auditChildProcessSourceSurface(discoveredPackage.rootPath)),
|
|
424
449
|
...(await auditPromptPoisoningSurface(discoveredPackage.rootPath)),
|
|
425
|
-
...(await auditSkillExternalReferences(discoveredPackage.rootPath
|
|
450
|
+
...(await auditSkillExternalReferences(discoveredPackage.rootPath, discoveredPackage.manifest.mcpServers
|
|
451
|
+
? path.resolve(discoveredPackage.rootPath, discoveredPackage.manifest.mcpServers)
|
|
452
|
+
: null))
|
|
426
453
|
];
|
|
427
454
|
return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
|
|
428
455
|
}
|