fullcourtdefense-cli 1.14.7 → 1.14.9

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.
@@ -39,6 +39,7 @@ const crypto = __importStar(require("crypto"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const os = __importStar(require("os"));
41
41
  const path = __importStar(require("path"));
42
+ const child_process_1 = require("child_process");
42
43
  const config_1 = require("../config");
43
44
  const discoverProxy_1 = require("./discoverProxy");
44
45
  const discoverSchedule_1 = require("./discoverSchedule");
@@ -227,7 +228,7 @@ const RISK_RULES = [
227
228
  { test: /stripe|payment|refund|payout|invoice|billing/i, level: 'critical', tag: 'payments' },
228
229
  { test: /filesystem|file[-_]?system|\bfs\b|read[-_]?file|write[-_]?file/i, level: 'high', tag: 'filesystem' },
229
230
  { test: /github|gitlab|bitbucket/i, level: 'high', tag: 'source-control' },
230
- { test: /aws|gcp|azure|cloud|kubernetes|k8s|terraform/i, level: 'high', tag: 'cloud-infra' },
231
+ { test: /aws|gcp|azure|cloud|kubernetes|k8s|terraform|docker|container|podman/i, level: 'high', tag: 'cloud-infra' },
231
232
  { test: /slack|email|gmail|smtp|sendgrid|twilio|notification/i, level: 'high', tag: 'messaging' },
232
233
  { test: /browser|puppeteer|playwright|fetch|http[-_]?request|web[-_]?search/i, level: 'medium', tag: 'web/network' },
233
234
  { test: /drive|gdrive|dropbox|s3|storage|notion|confluence/i, level: 'medium', tag: 'data-store' },
@@ -361,7 +362,94 @@ async function mcpJsonRpc(endpoint, method, params, timeoutMs, sessionId) {
361
362
  clearTimeout(timer);
362
363
  }
363
364
  }
364
- /** Best-effort live tools/list for HTTP/SSE MCP servers (stdio skipped — would spawn processes). */
365
+ function extractJsonRpcMessages(chunk) {
366
+ const messages = [];
367
+ let buffer = chunk;
368
+ while (true) {
369
+ const headerEnd = buffer.indexOf('\r\n\r\n');
370
+ if (headerEnd < 0)
371
+ break;
372
+ const header = buffer.slice(0, headerEnd);
373
+ const match = header.match(/Content-Length:\s*(\d+)/i);
374
+ if (!match)
375
+ break;
376
+ const length = Number(match[1]);
377
+ const bodyStart = headerEnd + 4;
378
+ const bodyEnd = bodyStart + length;
379
+ if (buffer.length < bodyEnd)
380
+ break;
381
+ try {
382
+ messages.push(JSON.parse(buffer.slice(bodyStart, bodyEnd)));
383
+ }
384
+ catch {
385
+ // Ignore malformed frames from noisy local servers.
386
+ }
387
+ buffer = buffer.slice(bodyEnd);
388
+ }
389
+ for (const line of chunk.split(/\r?\n/)) {
390
+ const trimmed = line.trim();
391
+ if (!trimmed.startsWith('{'))
392
+ continue;
393
+ try {
394
+ messages.push(JSON.parse(trimmed));
395
+ }
396
+ catch {
397
+ // Ignore logs or partial lines.
398
+ }
399
+ }
400
+ return messages;
401
+ }
402
+ function normalizeMcpToolList(tools) {
403
+ if (!Array.isArray(tools))
404
+ return [];
405
+ return tools
406
+ .map((t) => (t && typeof t.name === 'string'
407
+ ? { name: t.name, description: typeof t.description === 'string' ? t.description : undefined }
408
+ : null))
409
+ .filter(Boolean);
410
+ }
411
+ async function probeStdioMcpTools(command, args = [], timeoutMs = 8000) {
412
+ return new Promise(resolve => {
413
+ const child = (0, child_process_1.spawn)(command, args, {
414
+ cwd: process.cwd(),
415
+ env: process.env,
416
+ stdio: ['pipe', 'pipe', 'pipe'],
417
+ windowsHide: true,
418
+ });
419
+ let done = false;
420
+ const finish = (tools = []) => {
421
+ if (done)
422
+ return;
423
+ done = true;
424
+ clearTimeout(timer);
425
+ child.kill();
426
+ resolve(tools);
427
+ };
428
+ const timer = setTimeout(() => finish([]), timeoutMs);
429
+ child.on('error', () => finish([]));
430
+ child.on('exit', () => finish([]));
431
+ child.stdout.setEncoding('utf8');
432
+ child.stdout.on('data', chunk => {
433
+ for (const message of extractJsonRpcMessages(String(chunk))) {
434
+ if (message?.id !== 2)
435
+ continue;
436
+ const tools = normalizeMcpToolList(message?.result?.tools);
437
+ finish(tools);
438
+ }
439
+ });
440
+ const send = (id, method, params = {}) => {
441
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
442
+ };
443
+ send(1, 'initialize', {
444
+ protocolVersion: '2024-11-05',
445
+ capabilities: {},
446
+ clientInfo: { name: 'fullcourtdefense-discover', version: '1.0.0' },
447
+ });
448
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '\n');
449
+ send(2, 'tools/list');
450
+ });
451
+ }
452
+ /** Best-effort live tools/list for HTTP/SSE MCP servers. */
365
453
  async function probeHttpMcpTools(url, timeoutMs = 8000) {
366
454
  const normalized = url.replace(/\/$/, '');
367
455
  const candidates = [...new Set([
@@ -384,9 +472,7 @@ async function probeHttpMcpTools(url, timeoutMs = 8000) {
384
472
  const listed = await mcpJsonRpc(endpoint, 'tools/list', {}, timeoutMs, sid);
385
473
  const tools = listed.json?.result?.tools;
386
474
  if (Array.isArray(tools) && tools.length > 0) {
387
- return tools
388
- .map((t) => (t && typeof t.name === 'string' ? { name: t.name, description: typeof t.description === 'string' ? t.description : undefined } : null))
389
- .filter(Boolean);
475
+ return normalizeMcpToolList(tools);
390
476
  }
391
477
  }
392
478
  catch {
@@ -398,8 +484,23 @@ async function probeHttpMcpTools(url, timeoutMs = 8000) {
398
484
  async function enrichWithDeepProbe(servers, silent) {
399
485
  for (const server of servers) {
400
486
  if (server.transport !== 'http' && server.transport !== 'sse') {
401
- if (!silent && server.transport === 'stdio') {
402
- server.warnings.push('Deep probe skipped for stdio server (config-only inventory).');
487
+ if (server.transport === 'stdio' && server.command) {
488
+ const downstream = (0, discoverProxy_1.isFcdGatewayServer)(server) ? (0, discoverProxy_1.extractGatewayDownstream)(server) : null;
489
+ const command = downstream?.command || server.command;
490
+ const args = downstream?.args || server.args || [];
491
+ const liveTools = await probeStdioMcpTools(command, args);
492
+ if (liveTools.length > 0) {
493
+ const byName = new Map(server.tools.map(t => [t.name, t]));
494
+ for (const tool of liveTools)
495
+ byName.set(tool.name, tool);
496
+ server.tools = [...byName.values()];
497
+ server.probeMode = 'deep';
498
+ continue;
499
+ }
500
+ server.warnings.push('Deep probe could not list tools from stdio server (server may require auth or failed to start).');
501
+ }
502
+ else if (!silent && server.transport === 'stdio') {
503
+ server.warnings.push('Deep probe skipped for stdio server without a command.');
403
504
  }
404
505
  continue;
405
506
  }
@@ -678,7 +779,7 @@ async function discoverCommand(args, config) {
678
779
  clientCoverage = applyProxyClassification(found, scanned, cwd);
679
780
  if (deep && found.length > 0) {
680
781
  if (!silent)
681
- console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
782
+ console.log(`${COLOR.gray}Running deep probe (HTTP/SSE/stdio tools/list)…${COLOR.reset}`);
682
783
  await enrichWithDeepProbe(found, silent);
683
784
  }
684
785
  }
@@ -39,6 +39,51 @@ const KNOWN = [
39
39
  recommendGateway: true,
40
40
  },
41
41
  },
42
+ {
43
+ match: /docker|container|podman/i,
44
+ info: {
45
+ title: 'Container runtime',
46
+ description: 'Controls local containers or registry access from the AI client.',
47
+ category: 'other',
48
+ recommendGateway: true,
49
+ },
50
+ },
51
+ {
52
+ match: /kubernetes|k8s/i,
53
+ info: {
54
+ title: 'Kubernetes',
55
+ description: 'Controls Kubernetes resources from the AI client.',
56
+ category: 'other',
57
+ recommendGateway: true,
58
+ },
59
+ },
60
+ {
61
+ match: /aws|gcp|azure|terraform|cloud/i,
62
+ info: {
63
+ title: 'Cloud infrastructure',
64
+ description: 'Controls cloud infrastructure or deployment resources.',
65
+ category: 'other',
66
+ recommendGateway: true,
67
+ },
68
+ },
69
+ {
70
+ match: /slack|gmail|email|sendgrid|twilio/i,
71
+ info: {
72
+ title: 'Messaging',
73
+ description: 'Can send messages or notifications through external services.',
74
+ category: 'other',
75
+ recommendGateway: true,
76
+ },
77
+ },
78
+ {
79
+ match: /memory/i,
80
+ info: {
81
+ title: 'Memory',
82
+ description: 'Stores local agent memory; review content, but this is usually lower risk than shell, database, or cloud tools.',
83
+ category: 'other',
84
+ recommendGateway: false,
85
+ },
86
+ },
42
87
  {
43
88
  match: /agentguard-gateway|fullcourtdefense-gateway|fcd-gateway/i,
44
89
  info: {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.14.7"
2
+ "version": "1.14.9"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.14.7",
3
+ "version": "1.14.9",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -23,6 +23,7 @@
23
23
  "test:secret-posture": "npm run build && node scripts/test-secret-posture-fixtures.js",
24
24
  "test:agent-file-posture": "npm run build && node scripts/test-agent-file-posture.js",
25
25
  "test:realworld-posture": "npm run build && node scripts/test-realworld-posture-fixtures.js",
26
+ "test:discover-stdio-mcp": "npm run build && node scripts/test-discover-stdio-mcp-tools.js",
26
27
  "test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
27
28
  "test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
28
29
  "prepublishOnly": "npm run build"