fullcourtdefense-cli 1.1.17 → 1.2.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 +33 -0
- package/dist/commands/discover.d.ts +14 -0
- package/dist/commands/discover.js +245 -16
- package/dist/commands/discoverProxy.d.ts +42 -0
- package/dist/commands/discoverProxy.js +256 -0
- package/dist/commands/discoverSchedule.d.ts +6 -0
- package/dist/commands/discoverSchedule.js +132 -0
- package/dist/index.js +10 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -53,8 +53,41 @@ fullcourtdefense init
|
|
|
53
53
|
- `fullcourtdefense install-claude-code-mcp-gateway ...` — registers the protected MCP gateway in Claude Code.
|
|
54
54
|
- `fullcourtdefense install-claude-desktop-mcp-gateway ...` — registers the protected MCP gateway in Claude Desktop.
|
|
55
55
|
- `fullcourtdefense credits` — checks hosted scan credits for CI/CD API-key scans.
|
|
56
|
+
- `fullcourtdefense discover` — scans local MCP client configs and reports risk; `--upload` sends desktop inventory to AI Inventory.
|
|
56
57
|
- `fullcourtdefense init` — creates a starter config file.
|
|
57
58
|
|
|
59
|
+
## Shadow AI / Desktop Discovery
|
|
60
|
+
|
|
61
|
+
Find MCP servers configured on developer machines without reading chat history or spawning stdio servers by default:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Scan this machine and print results
|
|
65
|
+
fullcourtdefense discover
|
|
66
|
+
|
|
67
|
+
# JSON output (includes host fingerprint + servers)
|
|
68
|
+
fullcourtdefense discover --json
|
|
69
|
+
|
|
70
|
+
# Upload to AI Inventory (requires CI/CD API key with audit:write)
|
|
71
|
+
fullcourtdefense discover --upload --api-key YOUR_KEY
|
|
72
|
+
|
|
73
|
+
# Live-probe HTTP/SSE MCP servers for tools/list (stdio stays config-only)
|
|
74
|
+
fullcourtdefense discover --deep
|
|
75
|
+
|
|
76
|
+
# Fleet / MDM script — silent upload with user attribution
|
|
77
|
+
fullcourtdefense discover --upload --silent --user-email you@company.com --api-key YOUR_KEY
|
|
78
|
+
|
|
79
|
+
# Daily automatic scan on this laptop (uses API key from env or ~/.fullcourtdefense.yml)
|
|
80
|
+
fullcourtdefense discover --schedule daily --user-email you@company.com
|
|
81
|
+
|
|
82
|
+
# Remove daily schedule
|
|
83
|
+
fullcourtdefense discover --unschedule true
|
|
84
|
+
|
|
85
|
+
# Scan an extra config path
|
|
86
|
+
fullcourtdefense discover --path /path/to/mcp.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Each upload includes host metadata (`machineId`, `hostname`, `platform`, `user`) so the same MCP server on different laptops appears as separate desktop inventory rows. The CLI is also available as `fcd` or `botguard` after global install.
|
|
90
|
+
|
|
58
91
|
## Config File
|
|
59
92
|
|
|
60
93
|
Create a `.fullcourtdefense.yml` to avoid passing flags every time:
|
|
@@ -7,5 +7,19 @@ export interface DiscoverArgs {
|
|
|
7
7
|
upload?: string;
|
|
8
8
|
connectorName?: string;
|
|
9
9
|
extraPath?: string;
|
|
10
|
+
deep?: string;
|
|
11
|
+
silent?: string;
|
|
12
|
+
userEmail?: string;
|
|
13
|
+
schedule?: string;
|
|
14
|
+
unschedule?: string;
|
|
15
|
+
scheduleHour?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DesktopDiscoveryHost {
|
|
18
|
+
machineId: string;
|
|
19
|
+
hostname: string;
|
|
20
|
+
platform: string;
|
|
21
|
+
user: string;
|
|
22
|
+
scannedAt: string;
|
|
23
|
+
probeMode: 'config' | 'deep';
|
|
10
24
|
}
|
|
11
25
|
export declare function discoverCommand(args: DiscoverArgs, config: BotGuardConfig): Promise<void>;
|
|
@@ -34,12 +34,30 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.discoverCommand = discoverCommand;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const os = __importStar(require("os"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
41
|
+
const discoverProxy_1 = require("./discoverProxy");
|
|
42
|
+
const discoverSchedule_1 = require("./discoverSchedule");
|
|
40
43
|
const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
|
|
41
44
|
const SECRET_VALUE = /sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{20,}/;
|
|
42
45
|
const SECRET_KEY = /key|token|secret|password|credential|api[_-]?key/i;
|
|
46
|
+
function machineFingerprint() {
|
|
47
|
+
const seed = [os.hostname(), os.userInfo().username, os.platform(), os.arch()].join('|');
|
|
48
|
+
return crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16);
|
|
49
|
+
}
|
|
50
|
+
function buildHostMetadata(userEmail, probeMode = 'config') {
|
|
51
|
+
const info = os.userInfo();
|
|
52
|
+
return {
|
|
53
|
+
machineId: machineFingerprint(),
|
|
54
|
+
hostname: os.hostname(),
|
|
55
|
+
platform: os.platform(),
|
|
56
|
+
user: userEmail || info.username,
|
|
57
|
+
scannedAt: new Date().toISOString(),
|
|
58
|
+
probeMode,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
43
61
|
/** Where MCP client configs live, by client + OS. */
|
|
44
62
|
function candidateConfigPaths(cwd, extra) {
|
|
45
63
|
const home = os.homedir();
|
|
@@ -50,10 +68,25 @@ function candidateConfigPaths(cwd, extra) {
|
|
|
50
68
|
// Cursor
|
|
51
69
|
{ path: path.join(home, '.cursor', 'mcp.json'), source: 'Cursor (global)' },
|
|
52
70
|
{ path: path.join(cwd, '.cursor', 'mcp.json'), source: 'Cursor (project)' },
|
|
71
|
+
// Cursor managed (enterprise)
|
|
72
|
+
{ path: path.join(appData, 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, win)' },
|
|
73
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, mac)' },
|
|
74
|
+
{ path: '/etc/cursor/managed-mcp.json', source: 'Cursor (managed, linux)' },
|
|
53
75
|
// Claude Desktop
|
|
54
76
|
{ path: path.join(appData, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (win)' },
|
|
55
77
|
{ path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (mac)' },
|
|
56
78
|
{ path: path.join(xdg, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (linux)' },
|
|
79
|
+
// Claude Code
|
|
80
|
+
{ path: path.join(home, '.claude.json'), source: 'Claude Code' },
|
|
81
|
+
{ path: path.join(home, '.claude', 'settings.json'), source: 'Claude Code (global)' },
|
|
82
|
+
{ path: path.join(cwd, '.claude', 'settings.json'), source: 'Claude Code (project)' },
|
|
83
|
+
{ path: path.join(cwd, '.mcp.json'), source: 'Claude Code (.mcp.json project)' },
|
|
84
|
+
// Gemini CLI
|
|
85
|
+
{ path: path.join(home, '.gemini', 'settings.json'), source: 'Gemini CLI' },
|
|
86
|
+
{ path: path.join(xdg, 'gemini', 'settings.json'), source: 'Gemini CLI (xdg)' },
|
|
87
|
+
// OpenAI Codex
|
|
88
|
+
{ path: path.join(home, '.codex', 'mcp.json'), source: 'Codex' },
|
|
89
|
+
{ path: path.join(xdg, 'codex', 'mcp.json'), source: 'Codex (xdg)' },
|
|
57
90
|
// VS Code (project + user settings)
|
|
58
91
|
{ path: path.join(cwd, '.vscode', 'mcp.json'), source: 'VS Code (project)' },
|
|
59
92
|
{ path: path.join(appData, 'Code', 'User', 'settings.json'), source: 'VS Code (user, win)' },
|
|
@@ -61,11 +94,14 @@ function candidateConfigPaths(cwd, extra) {
|
|
|
61
94
|
{ path: path.join(xdg, 'Code', 'User', 'settings.json'), source: 'VS Code (user, linux)' },
|
|
62
95
|
// Windsurf
|
|
63
96
|
{ path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), source: 'Windsurf' },
|
|
97
|
+
// Kiro / generic
|
|
98
|
+
{ path: path.join(home, '.kiro', 'mcp.json'), source: 'Kiro' },
|
|
64
99
|
// Generic / repo-root
|
|
65
100
|
{ path: path.join(cwd, '.mcp.json'), source: 'Repo (.mcp.json)' },
|
|
66
101
|
{ path: path.join(cwd, 'mcp.json'), source: 'Repo (mcp.json)' },
|
|
67
102
|
];
|
|
68
103
|
if (process.platform === 'win32') {
|
|
104
|
+
list.push({ path: path.join(process.env.ProgramData || 'C:\\ProgramData', 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, program data)' });
|
|
69
105
|
const packagesDir = path.join(localAppData, 'Packages');
|
|
70
106
|
try {
|
|
71
107
|
for (const packageName of fs.readdirSync(packagesDir)) {
|
|
@@ -137,7 +173,7 @@ function parseConfigFile(filePath, source) {
|
|
|
137
173
|
parsed = JSON.parse(raw);
|
|
138
174
|
}
|
|
139
175
|
catch {
|
|
140
|
-
return [];
|
|
176
|
+
return [];
|
|
141
177
|
}
|
|
142
178
|
const servers = extractServerMap(parsed);
|
|
143
179
|
const out = [];
|
|
@@ -152,7 +188,15 @@ function parseConfigFile(filePath, source) {
|
|
|
152
188
|
? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
|
|
153
189
|
: command ? 'stdio' : 'unknown';
|
|
154
190
|
const tools = Array.isArray(cfg.tools)
|
|
155
|
-
? cfg.tools
|
|
191
|
+
? cfg.tools
|
|
192
|
+
.map((t) => {
|
|
193
|
+
if (typeof t === 'string')
|
|
194
|
+
return { name: t };
|
|
195
|
+
if (t && typeof t.name === 'string')
|
|
196
|
+
return { name: t.name, description: typeof t.description === 'string' ? t.description : undefined };
|
|
197
|
+
return null;
|
|
198
|
+
})
|
|
199
|
+
.filter(Boolean)
|
|
156
200
|
: [];
|
|
157
201
|
const { level, tags } = scoreRisk({ serverName: name, command, args, url });
|
|
158
202
|
const warnings = [];
|
|
@@ -178,10 +222,97 @@ function parseConfigFile(filePath, source) {
|
|
|
178
222
|
riskLevel: level,
|
|
179
223
|
riskTags: tags,
|
|
180
224
|
warnings,
|
|
225
|
+
probeMode: 'config',
|
|
226
|
+
proxyStatus: 'unknown',
|
|
181
227
|
});
|
|
182
228
|
}
|
|
183
229
|
return out;
|
|
184
230
|
}
|
|
231
|
+
async function mcpJsonRpc(endpoint, method, params, timeoutMs, sessionId) {
|
|
232
|
+
const headers = {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
Accept: 'application/json, text/event-stream',
|
|
235
|
+
};
|
|
236
|
+
if (sessionId)
|
|
237
|
+
headers['Mcp-Session-Id'] = sessionId;
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
240
|
+
try {
|
|
241
|
+
const resp = await fetch(endpoint, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers,
|
|
244
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
|
|
245
|
+
signal: controller.signal,
|
|
246
|
+
});
|
|
247
|
+
const nextSession = resp.headers.get('mcp-session-id') || sessionId;
|
|
248
|
+
const ct = resp.headers.get('content-type') || '';
|
|
249
|
+
if (ct.includes('text/event-stream')) {
|
|
250
|
+
const text = await resp.text();
|
|
251
|
+
const dataLine = text.split('\n').find(line => line.startsWith('data: '));
|
|
252
|
+
return { json: dataLine ? JSON.parse(dataLine.slice(6)) : null, sessionId: nextSession || undefined };
|
|
253
|
+
}
|
|
254
|
+
return { json: await resp.json(), sessionId: nextSession || undefined };
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Best-effort live tools/list for HTTP/SSE MCP servers (stdio skipped — would spawn processes). */
|
|
261
|
+
async function probeHttpMcpTools(url, timeoutMs = 8000) {
|
|
262
|
+
const normalized = url.replace(/\/$/, '');
|
|
263
|
+
const candidates = [...new Set([
|
|
264
|
+
normalized,
|
|
265
|
+
normalized.replace(/\/sse(\?.*)?$/i, ''),
|
|
266
|
+
`${normalized}/mcp`,
|
|
267
|
+
normalized.replace(/\/sse(\?.*)?$/i, '/mcp'),
|
|
268
|
+
])];
|
|
269
|
+
for (const endpoint of candidates) {
|
|
270
|
+
try {
|
|
271
|
+
const init = await mcpJsonRpc(endpoint, 'initialize', {
|
|
272
|
+
protocolVersion: '2024-11-05',
|
|
273
|
+
capabilities: {},
|
|
274
|
+
clientInfo: { name: 'fullcourtdefense-discover', version: '1.0.0' },
|
|
275
|
+
}, timeoutMs);
|
|
276
|
+
const sid = init.sessionId;
|
|
277
|
+
if (init.json?.error)
|
|
278
|
+
continue;
|
|
279
|
+
await mcpJsonRpc(endpoint, 'notifications/initialized', {}, timeoutMs, sid);
|
|
280
|
+
const listed = await mcpJsonRpc(endpoint, 'tools/list', {}, timeoutMs, sid);
|
|
281
|
+
const tools = listed.json?.result?.tools;
|
|
282
|
+
if (Array.isArray(tools) && tools.length > 0) {
|
|
283
|
+
return tools
|
|
284
|
+
.map((t) => (t && typeof t.name === 'string' ? { name: t.name, description: typeof t.description === 'string' ? t.description : undefined } : null))
|
|
285
|
+
.filter(Boolean);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// try next endpoint variant
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
async function enrichWithDeepProbe(servers, silent) {
|
|
295
|
+
for (const server of servers) {
|
|
296
|
+
if (server.transport !== 'http' && server.transport !== 'sse') {
|
|
297
|
+
if (!silent && server.transport === 'stdio') {
|
|
298
|
+
server.warnings.push('Deep probe skipped for stdio server (config-only inventory).');
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (!server.url)
|
|
303
|
+
continue;
|
|
304
|
+
const liveTools = await probeHttpMcpTools(server.url);
|
|
305
|
+
if (liveTools.length === 0) {
|
|
306
|
+
server.warnings.push('Deep probe could not list tools (server may require auth or be offline).');
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const byName = new Map(server.tools.map(t => [t.name, t]));
|
|
310
|
+
for (const tool of liveTools)
|
|
311
|
+
byName.set(tool.name, tool);
|
|
312
|
+
server.tools = [...byName.values()];
|
|
313
|
+
server.probeMode = 'deep';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
185
316
|
/** Merge servers found in multiple files; keep richest record, union sources. */
|
|
186
317
|
function dedupe(servers) {
|
|
187
318
|
const byName = new Map();
|
|
@@ -195,8 +326,13 @@ function dedupe(servers) {
|
|
|
195
326
|
existing.source += `, ${s.source}`;
|
|
196
327
|
existing.command = existing.command || s.command;
|
|
197
328
|
existing.url = existing.url || s.url;
|
|
198
|
-
|
|
329
|
+
const toolMap = new Map(existing.tools.map(t => [t.name, t]));
|
|
330
|
+
for (const t of s.tools)
|
|
331
|
+
toolMap.set(t.name, t);
|
|
332
|
+
existing.tools = [...toolMap.values()];
|
|
199
333
|
existing.warnings = Array.from(new Set([...existing.warnings, ...s.warnings]));
|
|
334
|
+
if (s.probeMode === 'deep')
|
|
335
|
+
existing.probeMode = 'deep';
|
|
200
336
|
}
|
|
201
337
|
return [...byName.values()];
|
|
202
338
|
}
|
|
@@ -207,14 +343,47 @@ const COLOR = {
|
|
|
207
343
|
function riskColor(level) {
|
|
208
344
|
return level === 'critical' || level === 'high' ? COLOR.red : level === 'medium' ? COLOR.yellow : COLOR.green;
|
|
209
345
|
}
|
|
210
|
-
|
|
346
|
+
function applyProxyClassification(servers, scanned, cwd) {
|
|
347
|
+
const wrapsByConfig = (0, discoverProxy_1.buildGatewayWrapIndex)(servers);
|
|
348
|
+
for (const server of servers) {
|
|
349
|
+
server.proxyStatus = (0, discoverProxy_1.classifyProxyStatus)(server, wrapsByConfig);
|
|
350
|
+
if (server.proxyStatus === 'fcd_gateway') {
|
|
351
|
+
server.wrapsServerName = (0, discoverProxy_1.extractGatewayDownstream)(server)?.label;
|
|
352
|
+
}
|
|
353
|
+
if (server.proxyStatus === 'direct' && server.riskLevel !== 'low') {
|
|
354
|
+
server.warnings.push('Direct MCP — not proxied through Full Court Defense gateway.');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return (0, discoverProxy_1.buildClientCoverage)(scanned, servers, cwd);
|
|
358
|
+
}
|
|
359
|
+
function proxyStatusLabel(status) {
|
|
360
|
+
if (status === 'fcd_gateway')
|
|
361
|
+
return 'FCD gateway';
|
|
362
|
+
if (status === 'proxied')
|
|
363
|
+
return 'Proxied';
|
|
364
|
+
if (status === 'direct')
|
|
365
|
+
return 'Direct — needs gateway';
|
|
366
|
+
return 'Unknown';
|
|
367
|
+
}
|
|
368
|
+
async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorName) {
|
|
211
369
|
const payload = {
|
|
212
|
-
connectorName: connectorName ||
|
|
370
|
+
connectorName: connectorName || `Desktop MCP · ${host.hostname}`,
|
|
371
|
+
host,
|
|
372
|
+
clientCoverage,
|
|
213
373
|
servers: servers.map(s => ({
|
|
214
374
|
serverName: s.serverName,
|
|
215
375
|
command: s.command,
|
|
216
376
|
url: s.url,
|
|
217
|
-
|
|
377
|
+
transport: s.transport,
|
|
378
|
+
clientSource: s.source,
|
|
379
|
+
configPath: s.configPath,
|
|
380
|
+
riskLevel: s.riskLevel,
|
|
381
|
+
riskTags: s.riskTags,
|
|
382
|
+
warnings: s.warnings,
|
|
383
|
+
probeMode: s.probeMode,
|
|
384
|
+
proxyStatus: s.proxyStatus,
|
|
385
|
+
wrapsServerName: s.wrapsServerName,
|
|
386
|
+
tools: s.tools,
|
|
218
387
|
})),
|
|
219
388
|
};
|
|
220
389
|
const resp = await fetch(`${apiUrl.replace(/\/$/, '')}/api/cicd/discovery/mcp`, {
|
|
@@ -227,12 +396,30 @@ async function upload(servers, apiUrl, apiKey, connectorName) {
|
|
|
227
396
|
throw new Error(data.error || `Upload failed (${resp.status})`);
|
|
228
397
|
}
|
|
229
398
|
const ingested = data.data?.ingested ?? servers.length;
|
|
230
|
-
console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) to your AI Inventory.${COLOR.reset}`);
|
|
399
|
+
console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) from ${host.hostname} to your AI Inventory.${COLOR.reset}`);
|
|
231
400
|
}
|
|
232
401
|
async function discoverCommand(args, config) {
|
|
233
402
|
const type = (args.type || 'mcp').toLowerCase();
|
|
403
|
+
const silent = args.silent === 'true';
|
|
404
|
+
const deep = args.deep === 'true';
|
|
405
|
+
if (args.unschedule === 'true') {
|
|
406
|
+
(0, discoverSchedule_1.uninstallDailyDiscoverSchedule)();
|
|
407
|
+
if (!silent)
|
|
408
|
+
console.log('Removed daily desktop discovery schedule.');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (args.schedule === 'daily') {
|
|
412
|
+
const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
|
|
413
|
+
(0, discoverSchedule_1.installDailyDiscoverSchedule)({ userEmail: args.userEmail, hour: Number.isFinite(hour) ? hour : 9 });
|
|
414
|
+
if (!silent) {
|
|
415
|
+
console.log(`Daily desktop discovery scheduled (runs discover --upload --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
|
|
416
|
+
console.log('Requires FULLCOURTDEFENSE_API_KEY or apiKey in ~/.fullcourtdefense.yml');
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
234
420
|
if (type !== 'mcp') {
|
|
235
|
-
|
|
421
|
+
if (!silent)
|
|
422
|
+
console.error(`Unsupported discover type "${type}". Supported: mcp`);
|
|
236
423
|
process.exit(1);
|
|
237
424
|
}
|
|
238
425
|
const cwd = process.cwd();
|
|
@@ -242,24 +429,49 @@ async function discoverCommand(args, config) {
|
|
|
242
429
|
for (const c of candidates) {
|
|
243
430
|
if (!fs.existsSync(c.path))
|
|
244
431
|
continue;
|
|
245
|
-
scanned.push(
|
|
432
|
+
scanned.push({ path: c.path, source: c.source });
|
|
246
433
|
found.push(...parseConfigFile(c.path, c.source));
|
|
247
434
|
}
|
|
248
435
|
found = dedupe(found);
|
|
436
|
+
const clientCoverage = applyProxyClassification(found, scanned, cwd);
|
|
437
|
+
if (deep && found.length > 0) {
|
|
438
|
+
if (!silent)
|
|
439
|
+
console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
|
|
440
|
+
await enrichWithDeepProbe(found, silent);
|
|
441
|
+
}
|
|
442
|
+
const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
|
|
249
443
|
if (args.json === 'true') {
|
|
250
|
-
console.log(JSON.stringify({
|
|
444
|
+
console.log(JSON.stringify({
|
|
445
|
+
host,
|
|
446
|
+
scannedFiles: scanned.map(s => `${s.source} → ${s.path}`),
|
|
447
|
+
clientCoverage,
|
|
448
|
+
servers: found,
|
|
449
|
+
}, null, 2));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (silent) {
|
|
453
|
+
if (args.upload === 'true') {
|
|
454
|
+
const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
|
|
455
|
+
const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
|
|
456
|
+
if (!apiKey)
|
|
457
|
+
process.exit(1);
|
|
458
|
+
if (found.length === 0)
|
|
459
|
+
return;
|
|
460
|
+
await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
|
|
461
|
+
}
|
|
251
462
|
return;
|
|
252
463
|
}
|
|
253
464
|
console.log(`\n${COLOR.bold}${COLOR.cyan}MCP server discovery${COLOR.reset}`);
|
|
465
|
+
console.log(`${COLOR.gray}Host ${host.hostname} · ${host.platform} · machine ${host.machineId}${COLOR.reset}`);
|
|
254
466
|
console.log(`${COLOR.gray}Scanned ${candidates.length} known config locations on this machine.${COLOR.reset}\n`);
|
|
255
467
|
if (scanned.length === 0) {
|
|
256
468
|
console.log(`${COLOR.yellow}No MCP config files found.${COLOR.reset}`);
|
|
257
|
-
console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop, VS Code, Windsurf, and repo-root configs.${COLOR.reset}`);
|
|
469
|
+
console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop/Code, VS Code, Windsurf, Codex, Gemini CLI, and repo-root configs.${COLOR.reset}`);
|
|
258
470
|
return;
|
|
259
471
|
}
|
|
260
472
|
console.log(`${COLOR.bold}Config files found:${COLOR.reset}`);
|
|
261
473
|
for (const s of scanned)
|
|
262
|
-
console.log(` ${COLOR.dim}•${COLOR.reset} ${s}`);
|
|
474
|
+
console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} → ${s.path}`);
|
|
263
475
|
console.log('');
|
|
264
476
|
if (found.length === 0) {
|
|
265
477
|
console.log(`${COLOR.yellow}Config files exist but declare no MCP servers.${COLOR.reset}`);
|
|
@@ -267,16 +479,30 @@ async function discoverCommand(args, config) {
|
|
|
267
479
|
}
|
|
268
480
|
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
269
481
|
found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
|
|
482
|
+
if (clientCoverage.length > 0) {
|
|
483
|
+
console.log(`${COLOR.bold}Client coverage:${COLOR.reset}`);
|
|
484
|
+
for (const row of clientCoverage) {
|
|
485
|
+
const gateway = row.mcpGatewayInstalled ? `${COLOR.green}gateway ✓${COLOR.reset}` : `${COLOR.red}no gateway${COLOR.reset}`;
|
|
486
|
+
const hooks = row.clientKey === 'cursor'
|
|
487
|
+
? (row.cursorHooksInstalled ? `${COLOR.green}hooks ✓ (${row.cursorHookEvents.join(', ')})${COLOR.reset}` : `${COLOR.yellow}no hooks${COLOR.reset}`)
|
|
488
|
+
: '';
|
|
489
|
+
console.log(` ${COLOR.dim}•${COLOR.reset} ${row.client} — ${row.mcpServerCount} MCP · ${gateway}${hooks ? ` · ${hooks}` : ''}`);
|
|
490
|
+
}
|
|
491
|
+
console.log('');
|
|
492
|
+
}
|
|
270
493
|
console.log(`${COLOR.bold}${found.length} MCP server(s) discovered:${COLOR.reset}\n`);
|
|
271
494
|
for (const s of found) {
|
|
272
495
|
const col = riskColor(s.riskLevel);
|
|
273
496
|
const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
|
|
274
|
-
|
|
497
|
+
const proxyCol = s.proxyStatus === 'direct' ? COLOR.red : s.proxyStatus === 'proxied' || s.proxyStatus === 'fcd_gateway' ? COLOR.green : COLOR.yellow;
|
|
498
|
+
console.log(` ${col}${COLOR.bold}[${s.riskLevel.toUpperCase()}]${COLOR.reset} ${COLOR.bold}${s.serverName}${COLOR.reset} ${COLOR.gray}(${s.transport}${s.probeMode === 'deep' ? ', deep' : ''})${COLOR.reset} ${proxyCol}${proxyStatusLabel(s.proxyStatus)}${COLOR.reset}`);
|
|
275
499
|
console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
|
|
276
500
|
if (s.riskTags.length)
|
|
277
501
|
console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
|
|
278
|
-
if (s.tools.length)
|
|
279
|
-
|
|
502
|
+
if (s.tools.length) {
|
|
503
|
+
const names = s.tools.map(t => t.name);
|
|
504
|
+
console.log(` ${COLOR.gray}tools:${COLOR.reset} ${names.slice(0, 12).join(', ')}${names.length > 12 ? ' …' : ''}`);
|
|
505
|
+
}
|
|
280
506
|
console.log(` ${COLOR.gray}from:${COLOR.reset} ${s.source}`);
|
|
281
507
|
for (const w of s.warnings)
|
|
282
508
|
console.log(` ${COLOR.red}⚠ ${w}${COLOR.reset}`);
|
|
@@ -292,9 +518,12 @@ async function discoverCommand(args, config) {
|
|
|
292
518
|
process.exit(1);
|
|
293
519
|
}
|
|
294
520
|
console.log('');
|
|
295
|
-
await upload(found, apiUrl, apiKey, args.connectorName);
|
|
521
|
+
await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
|
|
296
522
|
}
|
|
297
523
|
else {
|
|
298
524
|
console.log(`${COLOR.gray}Run with --upload to push these into your AI Inventory.${COLOR.reset}`);
|
|
525
|
+
if (!deep)
|
|
526
|
+
console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE servers for tools/list.${COLOR.reset}`);
|
|
527
|
+
console.log(`${COLOR.gray}Add --schedule daily for automatic daily upload (MDM/fleet).${COLOR.reset}`);
|
|
299
528
|
}
|
|
300
529
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type DesktopProxyStatus = 'fcd_gateway' | 'proxied' | 'direct' | 'unknown';
|
|
2
|
+
export declare const FCD_GATEWAY_SERVER_NAMES: Set<string>;
|
|
3
|
+
export declare const FCD_HOOK_MARKER = "--fcd-managed true";
|
|
4
|
+
export declare const FCD_HOOK_TAG = "fullcourtdefense";
|
|
5
|
+
export interface ClientCoverageRow {
|
|
6
|
+
client: string;
|
|
7
|
+
clientKey: string;
|
|
8
|
+
configPath: string;
|
|
9
|
+
configPresent: boolean;
|
|
10
|
+
mcpServerCount: number;
|
|
11
|
+
mcpGatewayInstalled: boolean;
|
|
12
|
+
cursorHooksInstalled: boolean;
|
|
13
|
+
cursorHookEvents: string[];
|
|
14
|
+
cursorHookShadow: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ProxyClassifiableServer {
|
|
17
|
+
serverName: string;
|
|
18
|
+
command?: string;
|
|
19
|
+
args?: string[];
|
|
20
|
+
url?: string;
|
|
21
|
+
source: string;
|
|
22
|
+
configPath: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function isFcdGatewayServer(server: ProxyClassifiableServer): boolean;
|
|
25
|
+
export declare function extractGatewayDownstream(server: ProxyClassifiableServer): {
|
|
26
|
+
command?: string;
|
|
27
|
+
args?: string[];
|
|
28
|
+
label?: string;
|
|
29
|
+
} | null;
|
|
30
|
+
/** Per-client downstream targets wrapped by FCD gateway entries in the same config file. */
|
|
31
|
+
export declare function buildGatewayWrapIndex(servers: ProxyClassifiableServer[]): Map<string, Set<string>>;
|
|
32
|
+
export declare function classifyProxyStatus(server: ProxyClassifiableServer, wrapsByConfig: Map<string, Set<string>>): DesktopProxyStatus;
|
|
33
|
+
export declare function scanCursorHooks(projectPath?: string): {
|
|
34
|
+
installed: boolean;
|
|
35
|
+
events: string[];
|
|
36
|
+
shadow: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function clientKeyFromSource(source: string): string;
|
|
39
|
+
export declare function buildClientCoverage(scanned: Array<{
|
|
40
|
+
path: string;
|
|
41
|
+
source: string;
|
|
42
|
+
}>, servers: ProxyClassifiableServer[], cwd: string): ClientCoverageRow[];
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FCD_HOOK_TAG = exports.FCD_HOOK_MARKER = exports.FCD_GATEWAY_SERVER_NAMES = void 0;
|
|
37
|
+
exports.isFcdGatewayServer = isFcdGatewayServer;
|
|
38
|
+
exports.extractGatewayDownstream = extractGatewayDownstream;
|
|
39
|
+
exports.buildGatewayWrapIndex = buildGatewayWrapIndex;
|
|
40
|
+
exports.classifyProxyStatus = classifyProxyStatus;
|
|
41
|
+
exports.scanCursorHooks = scanCursorHooks;
|
|
42
|
+
exports.clientKeyFromSource = clientKeyFromSource;
|
|
43
|
+
exports.buildClientCoverage = buildClientCoverage;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
exports.FCD_GATEWAY_SERVER_NAMES = new Set([
|
|
48
|
+
'agentguard-gateway',
|
|
49
|
+
'fullcourtdefense-gateway',
|
|
50
|
+
'fcd-gateway',
|
|
51
|
+
]);
|
|
52
|
+
exports.FCD_HOOK_MARKER = '--fcd-managed true';
|
|
53
|
+
exports.FCD_HOOK_TAG = 'fullcourtdefense';
|
|
54
|
+
function commandBlob(server) {
|
|
55
|
+
return `${server.serverName} ${server.command || ''} ${(server.args || []).join(' ')} ${server.url || ''}`.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
function isFcdGatewayServer(server) {
|
|
58
|
+
const name = server.serverName.toLowerCase();
|
|
59
|
+
if (exports.FCD_GATEWAY_SERVER_NAMES.has(name))
|
|
60
|
+
return true;
|
|
61
|
+
const blob = commandBlob(server);
|
|
62
|
+
return blob.includes('mcp-gateway')
|
|
63
|
+
&& (blob.includes('fullcourtdefense') || blob.includes('agentguard') || blob.includes('botguard') || blob.includes('fcd-managed'));
|
|
64
|
+
}
|
|
65
|
+
function extractGatewayDownstream(server) {
|
|
66
|
+
const args = server.args || [];
|
|
67
|
+
const cmdIdx = args.indexOf('--mcp-command');
|
|
68
|
+
if (cmdIdx < 0 || !args[cmdIdx + 1])
|
|
69
|
+
return null;
|
|
70
|
+
let downstreamArgs = [];
|
|
71
|
+
const argsIdx = args.indexOf('--mcp-args');
|
|
72
|
+
if (argsIdx >= 0 && args[argsIdx + 1]) {
|
|
73
|
+
try {
|
|
74
|
+
downstreamArgs = JSON.parse(args[argsIdx + 1]);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
downstreamArgs = [args[argsIdx + 1]];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const command = args[cmdIdx + 1];
|
|
81
|
+
const label = inferDownstreamLabel(command, downstreamArgs);
|
|
82
|
+
return { command, args: downstreamArgs, label };
|
|
83
|
+
}
|
|
84
|
+
function inferDownstreamLabel(command, args) {
|
|
85
|
+
const hay = `${command || ''} ${(args || []).join(' ')}`.toLowerCase();
|
|
86
|
+
const pkgMatch = hay.match(/@([a-z0-9._-]+\/mcp[a-z0-9._-]*|[a-z0-9._-]+-mcp[a-z0-9._-]*)/i);
|
|
87
|
+
if (pkgMatch)
|
|
88
|
+
return pkgMatch[1].replace(/^@/, '').split('@')[0];
|
|
89
|
+
const fileMatch = hay.match(/([^/\\]+\.(js|ts|py|mjs))(?:\s|$)/i);
|
|
90
|
+
if (fileMatch)
|
|
91
|
+
return fileMatch[1].replace(/\.(js|ts|py|mjs)$/i, '');
|
|
92
|
+
if (command)
|
|
93
|
+
return path.basename(command).replace(/\.(cmd|exe|js)$/i, '');
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
function downstreamFingerprint(command, args) {
|
|
97
|
+
return `${command || ''}|${(args || []).join(' ')}`.trim().toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
function serverFingerprints(server) {
|
|
100
|
+
const out = new Set();
|
|
101
|
+
out.add(server.serverName.toLowerCase());
|
|
102
|
+
const blob = commandBlob(server);
|
|
103
|
+
out.add(blob);
|
|
104
|
+
if (server.command)
|
|
105
|
+
out.add(downstreamFingerprint(server.command, server.args));
|
|
106
|
+
const label = inferDownstreamLabel(server.command, server.args);
|
|
107
|
+
if (label)
|
|
108
|
+
out.add(label.toLowerCase());
|
|
109
|
+
return [...out];
|
|
110
|
+
}
|
|
111
|
+
/** Per-client downstream targets wrapped by FCD gateway entries in the same config file. */
|
|
112
|
+
function buildGatewayWrapIndex(servers) {
|
|
113
|
+
const byConfig = new Map();
|
|
114
|
+
for (const server of servers) {
|
|
115
|
+
const key = server.configPath.toLowerCase();
|
|
116
|
+
const list = byConfig.get(key) || [];
|
|
117
|
+
list.push(server);
|
|
118
|
+
byConfig.set(key, list);
|
|
119
|
+
}
|
|
120
|
+
const wrapsByConfig = new Map();
|
|
121
|
+
for (const [configPath, list] of byConfig.entries()) {
|
|
122
|
+
const wrapped = new Set();
|
|
123
|
+
for (const server of list) {
|
|
124
|
+
if (!isFcdGatewayServer(server))
|
|
125
|
+
continue;
|
|
126
|
+
const downstream = extractGatewayDownstream(server);
|
|
127
|
+
if (downstream?.label)
|
|
128
|
+
wrapped.add(downstream.label.toLowerCase());
|
|
129
|
+
if (downstream?.command)
|
|
130
|
+
wrapped.add(downstreamFingerprint(downstream.command, downstream.args));
|
|
131
|
+
wrapped.add(server.serverName.toLowerCase());
|
|
132
|
+
}
|
|
133
|
+
wrapsByConfig.set(configPath, wrapped);
|
|
134
|
+
}
|
|
135
|
+
return wrapsByConfig;
|
|
136
|
+
}
|
|
137
|
+
function classifyProxyStatus(server, wrapsByConfig) {
|
|
138
|
+
if (isFcdGatewayServer(server))
|
|
139
|
+
return 'fcd_gateway';
|
|
140
|
+
const wrapped = wrapsByConfig.get(server.configPath.toLowerCase());
|
|
141
|
+
if (!wrapped || wrapped.size === 0)
|
|
142
|
+
return 'direct';
|
|
143
|
+
for (const fp of serverFingerprints(server)) {
|
|
144
|
+
if (wrapped.has(fp))
|
|
145
|
+
return 'proxied';
|
|
146
|
+
for (const w of wrapped) {
|
|
147
|
+
if (fp.includes(w) || w.includes(fp))
|
|
148
|
+
return 'proxied';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return 'direct';
|
|
152
|
+
}
|
|
153
|
+
const CURSOR_HOOK_EVENTS = {
|
|
154
|
+
beforeSubmitPrompt: 'prompt',
|
|
155
|
+
beforeShellExecution: 'shell',
|
|
156
|
+
beforeMCPExecution: 'mcp',
|
|
157
|
+
afterFileEdit: 'file',
|
|
158
|
+
beforeReadFile: 'read',
|
|
159
|
+
};
|
|
160
|
+
function readHooksJson(file) {
|
|
161
|
+
if (!fs.existsSync(file))
|
|
162
|
+
return { hooks: {} };
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
165
|
+
return { hooks: parsed?.hooks && typeof parsed.hooks === 'object' ? parsed.hooks : {} };
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { hooks: {} };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function scanCursorHooks(projectPath) {
|
|
172
|
+
const globalFile = path.join(os.homedir(), '.cursor', 'hooks.json');
|
|
173
|
+
const projectFile = projectPath ? path.join(projectPath, '.cursor', 'hooks.json') : '';
|
|
174
|
+
const files = [globalFile, projectFile].filter(Boolean);
|
|
175
|
+
const events = new Set();
|
|
176
|
+
let shadow = false;
|
|
177
|
+
let installed = false;
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const { hooks } = readHooksJson(file);
|
|
180
|
+
for (const [hookKey, entries] of Object.entries(hooks)) {
|
|
181
|
+
for (const entry of entries || []) {
|
|
182
|
+
const cmd = entry?.command || '';
|
|
183
|
+
if (!cmd.includes(exports.FCD_HOOK_MARKER) && !cmd.includes(exports.FCD_HOOK_TAG))
|
|
184
|
+
continue;
|
|
185
|
+
installed = true;
|
|
186
|
+
const mapped = CURSOR_HOOK_EVENTS[hookKey] || hookKey;
|
|
187
|
+
events.add(mapped);
|
|
188
|
+
if (cmd.includes('--shadow true'))
|
|
189
|
+
shadow = true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { installed, events: [...events].sort(), shadow };
|
|
194
|
+
}
|
|
195
|
+
function clientKeyFromSource(source) {
|
|
196
|
+
const s = source.toLowerCase();
|
|
197
|
+
if (s.includes('cursor'))
|
|
198
|
+
return 'cursor';
|
|
199
|
+
if (s.includes('claude desktop'))
|
|
200
|
+
return 'claude_desktop';
|
|
201
|
+
if (s.includes('claude code'))
|
|
202
|
+
return 'claude_code';
|
|
203
|
+
if (s.includes('vs code'))
|
|
204
|
+
return 'vscode';
|
|
205
|
+
if (s.includes('windsurf'))
|
|
206
|
+
return 'windsurf';
|
|
207
|
+
if (s.includes('gemini'))
|
|
208
|
+
return 'gemini_cli';
|
|
209
|
+
if (s.includes('codex'))
|
|
210
|
+
return 'codex';
|
|
211
|
+
if (s.includes('kiro'))
|
|
212
|
+
return 'kiro';
|
|
213
|
+
return 'other';
|
|
214
|
+
}
|
|
215
|
+
function buildClientCoverage(scanned, servers, cwd) {
|
|
216
|
+
const hooks = scanCursorHooks(cwd);
|
|
217
|
+
const serversByConfig = new Map();
|
|
218
|
+
for (const server of servers) {
|
|
219
|
+
const key = server.configPath.toLowerCase();
|
|
220
|
+
const list = serversByConfig.get(key) || [];
|
|
221
|
+
list.push(server);
|
|
222
|
+
serversByConfig.set(key, list);
|
|
223
|
+
}
|
|
224
|
+
const rows = [];
|
|
225
|
+
for (const item of scanned) {
|
|
226
|
+
const list = serversByConfig.get(item.path.toLowerCase()) || [];
|
|
227
|
+
const clientKey = clientKeyFromSource(item.source);
|
|
228
|
+
const mcpGatewayInstalled = list.some(isFcdGatewayServer);
|
|
229
|
+
rows.push({
|
|
230
|
+
client: item.source,
|
|
231
|
+
clientKey,
|
|
232
|
+
configPath: item.path,
|
|
233
|
+
configPresent: true,
|
|
234
|
+
mcpServerCount: list.filter(s => !isFcdGatewayServer(s)).length,
|
|
235
|
+
mcpGatewayInstalled,
|
|
236
|
+
cursorHooksInstalled: clientKey === 'cursor' ? hooks.installed : false,
|
|
237
|
+
cursorHookEvents: clientKey === 'cursor' ? hooks.events : [],
|
|
238
|
+
cursorHookShadow: clientKey === 'cursor' ? hooks.shadow : false,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Cursor hooks apply globally even if only project mcp.json was scanned
|
|
242
|
+
if (!rows.some(r => r.clientKey === 'cursor') && hooks.installed) {
|
|
243
|
+
rows.push({
|
|
244
|
+
client: 'Cursor (hooks)',
|
|
245
|
+
clientKey: 'cursor',
|
|
246
|
+
configPath: path.join(os.homedir(), '.cursor', 'hooks.json'),
|
|
247
|
+
configPresent: fs.existsSync(path.join(os.homedir(), '.cursor', 'hooks.json')),
|
|
248
|
+
mcpServerCount: 0,
|
|
249
|
+
mcpGatewayInstalled: false,
|
|
250
|
+
cursorHooksInstalled: true,
|
|
251
|
+
cursorHookEvents: hooks.events,
|
|
252
|
+
cursorHookShadow: hooks.shadow,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return rows.sort((a, b) => a.client.localeCompare(b.client));
|
|
256
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.installDailyDiscoverSchedule = installDailyDiscoverSchedule;
|
|
37
|
+
exports.uninstallDailyDiscoverSchedule = uninstallDailyDiscoverSchedule;
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const TASK_NAME = 'FullCourtDefenseDesktopDiscover';
|
|
43
|
+
function discoverCommandLine(userEmail) {
|
|
44
|
+
const node = process.execPath;
|
|
45
|
+
const script = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
|
|
46
|
+
const q = (value) => (/\s/.test(value) ? `"${value}"` : value);
|
|
47
|
+
const parts = [q(node), q(script), 'discover', '--upload', '--silent'];
|
|
48
|
+
if (userEmail)
|
|
49
|
+
parts.push('--user-email', q(userEmail));
|
|
50
|
+
return parts.join(' ');
|
|
51
|
+
}
|
|
52
|
+
function installWindowsSchedule(hour, userEmail) {
|
|
53
|
+
const tr = discoverCommandLine(userEmail);
|
|
54
|
+
const st = `${String(hour).padStart(2, '0')}:00`;
|
|
55
|
+
try {
|
|
56
|
+
(0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'ignore' });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// task may not exist
|
|
60
|
+
}
|
|
61
|
+
(0, child_process_1.execSync)(`schtasks /Create /F /TN "${TASK_NAME}" /SC DAILY /ST ${st} /TR "${tr.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
|
|
62
|
+
}
|
|
63
|
+
function installMacSchedule(hour, userEmail) {
|
|
64
|
+
const label = 'ai.fullcourtdefense.discover';
|
|
65
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
66
|
+
const plistPath = path.join(plistDir, `${label}.plist`);
|
|
67
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
68
|
+
const cmd = discoverCommandLine(userEmail).split(/\s+/);
|
|
69
|
+
const program = cmd.shift() || process.execPath;
|
|
70
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
71
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
72
|
+
<plist version="1.0">
|
|
73
|
+
<dict>
|
|
74
|
+
<key>Label</key><string>${label}</string>
|
|
75
|
+
<key>ProgramArguments</key>
|
|
76
|
+
<array>${[program, ...cmd].map(part => `<string>${part.replace(/&/g, '&').replace(/</g, '<')}</string>`).join('')}</array>
|
|
77
|
+
<key>StartCalendarInterval</key>
|
|
78
|
+
<dict><key>Hour</key><integer>${hour}</integer><key>Minute</key><integer>0</integer></dict>
|
|
79
|
+
<key>StandardOutPath</key><string>${path.join(os.homedir(), '.fullcourtdefense-discover.log')}</string>
|
|
80
|
+
<key>StandardErrorPath</key><string>${path.join(os.homedir(), '.fullcourtdefense-discover.err.log')}</string>
|
|
81
|
+
</dict>
|
|
82
|
+
</plist>`;
|
|
83
|
+
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
84
|
+
(0, child_process_1.spawnSync)('launchctl', ['unload', plistPath], { stdio: 'ignore' });
|
|
85
|
+
(0, child_process_1.spawnSync)('launchctl', ['load', plistPath], { stdio: 'inherit' });
|
|
86
|
+
}
|
|
87
|
+
function installLinuxCron(hour, userEmail) {
|
|
88
|
+
const line = `0 ${hour} * * * ${discoverCommandLine(userEmail)} >> ${path.join(os.homedir(), '.fullcourtdefense-discover.log')} 2>&1`;
|
|
89
|
+
const marker = '# fullcourtdefense-discover';
|
|
90
|
+
let crontab = '';
|
|
91
|
+
try {
|
|
92
|
+
crontab = (0, child_process_1.execSync)('crontab -l', { encoding: 'utf8' });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
crontab = '';
|
|
96
|
+
}
|
|
97
|
+
const filtered = crontab.split('\n').filter(l => !l.includes(marker) && !l.includes('discover --upload --silent'));
|
|
98
|
+
filtered.push(`${marker}`);
|
|
99
|
+
filtered.push(line);
|
|
100
|
+
(0, child_process_1.execSync)(`echo "${filtered.join('\\n')}" | crontab -`, { stdio: 'inherit', shell: '/bin/bash' });
|
|
101
|
+
}
|
|
102
|
+
function installDailyDiscoverSchedule(args = {}) {
|
|
103
|
+
const hour = Number.isFinite(args.hour) ? Math.min(23, Math.max(0, args.hour)) : 9;
|
|
104
|
+
if (process.platform === 'win32')
|
|
105
|
+
installWindowsSchedule(hour, args.userEmail);
|
|
106
|
+
else if (process.platform === 'darwin')
|
|
107
|
+
installMacSchedule(hour, args.userEmail);
|
|
108
|
+
else
|
|
109
|
+
installLinuxCron(hour, args.userEmail);
|
|
110
|
+
}
|
|
111
|
+
function uninstallDailyDiscoverSchedule() {
|
|
112
|
+
if (process.platform === 'win32') {
|
|
113
|
+
(0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'inherit' });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (process.platform === 'darwin') {
|
|
117
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.fullcourtdefense.discover.plist');
|
|
118
|
+
(0, child_process_1.spawnSync)('launchctl', ['unload', plistPath], { stdio: 'ignore' });
|
|
119
|
+
if (fs.existsSync(plistPath))
|
|
120
|
+
fs.unlinkSync(plistPath);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let crontab = '';
|
|
124
|
+
try {
|
|
125
|
+
crontab = (0, child_process_1.execSync)('crontab -l', { encoding: 'utf8' });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const filtered = crontab.split('\n').filter(l => !l.includes('fullcourtdefense-discover') && !l.includes('discover --upload --silent'));
|
|
131
|
+
(0, child_process_1.execSync)(`echo "${filtered.join('\\n')}" | crontab -`, { stdio: 'inherit', shell: '/bin/bash' });
|
|
132
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -66,8 +66,9 @@ function printHelp() {
|
|
|
66
66
|
configure Saves Shield ID, Shield key, and API URL to .fullcourtdefense.yml.
|
|
67
67
|
scan Runs the scan. Use --local for inside-organization scans.
|
|
68
68
|
discover Finds MCP servers configured on this machine (Cursor, Claude,
|
|
69
|
-
VS Code, Windsurf) and reports their risk.
|
|
70
|
-
them to your AI Inventory.
|
|
69
|
+
VS Code, Windsurf, Codex, Gemini CLI) and reports their risk.
|
|
70
|
+
Use --upload to add them to your AI Inventory. Use --deep for
|
|
71
|
+
live HTTP/SSE tools/list. Use --silent for fleet/MDM scripts.
|
|
71
72
|
install-cursor-hook
|
|
72
73
|
Installs a Cursor hook so every AI prompt + agent action
|
|
73
74
|
(shell, MCP) on this machine is scanned by your Shield.
|
|
@@ -199,6 +200,7 @@ function printHelp() {
|
|
|
199
200
|
$ fullcourtdefense scan --local --type mcp --mcp-command node --mcp-args ./server.js --mcp-tool all --mode full --format report
|
|
200
201
|
$ fullcourtdefense discover
|
|
201
202
|
$ fullcourtdefense discover --upload
|
|
203
|
+
$ fullcourtdefense discover --deep --upload --silent --user-email you@company.com
|
|
202
204
|
$ fullcourtdefense discover --type mcp --json
|
|
203
205
|
$ fullcourtdefense install-cursor-hook --shield-id <id> --shield-key <key>
|
|
204
206
|
$ fullcourtdefense install-cursor-hook --shadow true # monitor only
|
|
@@ -320,6 +322,12 @@ async function main() {
|
|
|
320
322
|
upload: flags.upload,
|
|
321
323
|
connectorName: flags['connector-name'],
|
|
322
324
|
extraPath: flags.path,
|
|
325
|
+
deep: flags.deep,
|
|
326
|
+
silent: flags.silent,
|
|
327
|
+
userEmail: flags['user-email'],
|
|
328
|
+
schedule: flags.schedule,
|
|
329
|
+
unschedule: flags.unschedule,
|
|
330
|
+
scheduleHour: flags['schedule-hour'],
|
|
323
331
|
};
|
|
324
332
|
await (0, discover_1.discoverCommand)(args, config);
|
|
325
333
|
break;
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"fullcourtdefense": "dist/index.js",
|
|
8
|
+
"fcd": "dist/index.js",
|
|
8
9
|
"botguard": "dist/index.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|