fullcourtdefense-cli 1.14.8 → 1.14.10
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/dist/commands/discover.js +115 -13
- package/dist/commands/discoverSchedule.js +1 -1
- package/dist/version.json +1 -1
- package/package.json +2 -1
|
@@ -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");
|
|
@@ -361,7 +362,94 @@ async function mcpJsonRpc(endpoint, method, params, timeoutMs, sessionId) {
|
|
|
361
362
|
clearTimeout(timer);
|
|
362
363
|
}
|
|
363
364
|
}
|
|
364
|
-
|
|
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 (
|
|
402
|
-
|
|
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
|
}
|
|
@@ -638,7 +739,8 @@ function printPostureReport(posture, silent) {
|
|
|
638
739
|
async function discoverCommand(args, config) {
|
|
639
740
|
const surfaces = parseSurfaces(args);
|
|
640
741
|
const silent = args.silent === 'true';
|
|
641
|
-
const
|
|
742
|
+
const uploadRequested = args.upload === 'true';
|
|
743
|
+
const deep = args.deep === 'false' ? false : (args.deep === 'true' || uploadRequested);
|
|
642
744
|
if (args.unschedule === 'true') {
|
|
643
745
|
(0, discoverSchedule_1.uninstallDailyDiscoverSchedule)();
|
|
644
746
|
if (!silent)
|
|
@@ -656,7 +758,7 @@ async function discoverCommand(args, config) {
|
|
|
656
758
|
});
|
|
657
759
|
if (!silent) {
|
|
658
760
|
const when = args.schedule === 'logon' ? 'at logon' : `daily at ${String(Number.isFinite(hour) ? hour : 9).padStart(2, '0')}:00`;
|
|
659
|
-
console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
|
|
761
|
+
console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --deep --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
|
|
660
762
|
console.log('Uses login Shield credentials or an explicit org API key.');
|
|
661
763
|
}
|
|
662
764
|
return;
|
|
@@ -678,7 +780,7 @@ async function discoverCommand(args, config) {
|
|
|
678
780
|
clientCoverage = applyProxyClassification(found, scanned, cwd);
|
|
679
781
|
if (deep && found.length > 0) {
|
|
680
782
|
if (!silent)
|
|
681
|
-
console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
|
|
783
|
+
console.log(`${COLOR.gray}Running deep probe (HTTP/SSE/stdio tools/list)…${COLOR.reset}`);
|
|
682
784
|
await enrichWithDeepProbe(found, silent);
|
|
683
785
|
}
|
|
684
786
|
}
|
|
@@ -736,7 +838,7 @@ async function discoverCommand(args, config) {
|
|
|
736
838
|
},
|
|
737
839
|
};
|
|
738
840
|
async function maybeUpload() {
|
|
739
|
-
if (
|
|
841
|
+
if (!uploadRequested)
|
|
740
842
|
return;
|
|
741
843
|
const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
|
|
742
844
|
// Either an org API key OR an enrolled Shield key (from `login`) is enough.
|
|
@@ -750,7 +852,7 @@ async function discoverCommand(args, config) {
|
|
|
750
852
|
await upload(found, host, clientCoverage, creds.apiUrl, { apiKey: creds.apiKey, shieldId: creds.shieldId, shieldKey: creds.shieldKey, preferApiKey: !!args.apiKey }, args.connectorName, uploadExtras);
|
|
751
853
|
}
|
|
752
854
|
if (silent) {
|
|
753
|
-
if (
|
|
855
|
+
if (uploadRequested) {
|
|
754
856
|
const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
|
|
755
857
|
if (!creds.apiKey && !(creds.shieldId && creds.shieldKey))
|
|
756
858
|
process.exit(1);
|
|
@@ -802,14 +904,14 @@ async function discoverCommand(args, config) {
|
|
|
802
904
|
printAgentFilesReport(agentFiles, silent);
|
|
803
905
|
if (posture)
|
|
804
906
|
printPostureReport(posture, silent);
|
|
805
|
-
if (
|
|
907
|
+
if (uploadRequested) {
|
|
806
908
|
console.log('');
|
|
807
909
|
await maybeUpload();
|
|
808
910
|
}
|
|
809
911
|
else {
|
|
810
912
|
console.log(`${COLOR.gray}Run with --upload to push results into AI Fleet.${COLOR.reset}`);
|
|
811
913
|
if (runMcp && !deep)
|
|
812
|
-
console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE MCP servers.${COLOR.reset}`);
|
|
914
|
+
console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE/stdio MCP servers.${COLOR.reset}`);
|
|
813
915
|
if (!surfaces.has('secrets'))
|
|
814
916
|
console.log(`${COLOR.gray}Add --surface secrets for .env / credential store scan (local-only).${COLOR.reset}`);
|
|
815
917
|
if (surfaces.size < 4)
|
|
@@ -49,7 +49,7 @@ function discoverCommandLine(userEmail, surface) {
|
|
|
49
49
|
const node = process.execPath;
|
|
50
50
|
const script = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
|
|
51
51
|
const q = (value) => (/\s/.test(value) ? `"${value}"` : value);
|
|
52
|
-
const parts = [q(node), q(script), 'discover', '--upload', '--silent'];
|
|
52
|
+
const parts = [q(node), q(script), 'discover', '--upload', '--deep', '--silent'];
|
|
53
53
|
if (surface)
|
|
54
54
|
parts.push('--surface', q(surface));
|
|
55
55
|
if (userEmail)
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.10",
|
|
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"
|