fullcourtdefense-cli 1.3.0 → 1.3.2

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.
@@ -38,11 +38,29 @@ exports.runDiscoverUpload = runDiscoverUpload;
38
38
  const crypto = __importStar(require("crypto"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const os = __importStar(require("os"));
41
+ const config_1 = require("../config");
41
42
  const discoverProxy_1 = require("./discoverProxy");
42
43
  const discoverSchedule_1 = require("./discoverSchedule");
43
44
  const discoverPaths_1 = require("./discoverPaths");
44
45
  const knownMcpServers_1 = require("./knownMcpServers");
46
+ const discoverAgentFiles_1 = require("./discoverAgentFiles");
47
+ const discoverBlastRadius_1 = require("./discoverBlastRadius");
48
+ const discoverSecrets_1 = require("./discoverSecrets");
45
49
  const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
50
+ function parseSurfaces(args) {
51
+ const raw = (args.surface || args.type || 'mcp').toLowerCase().trim();
52
+ if (raw === 'all' || raw === 'full') {
53
+ return new Set(['mcp', 'secrets', 'agent-files', 'posture']);
54
+ }
55
+ const allowed = ['mcp', 'secrets', 'agent-files', 'posture'];
56
+ const picked = raw.split(',').map(part => part.trim()).filter(Boolean);
57
+ const surfaces = new Set();
58
+ for (const part of picked) {
59
+ if (allowed.includes(part))
60
+ surfaces.add(part);
61
+ }
62
+ return surfaces.size > 0 ? surfaces : new Set(['mcp']);
63
+ }
46
64
  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,}/;
47
65
  const SECRET_KEY = /key|token|secret|password|credential|api[_-]?key/i;
48
66
  function machineFingerprint() {
@@ -103,6 +121,55 @@ function scoreRisk(s) {
103
121
  }
104
122
  return { level, tags };
105
123
  }
124
+ function pushDiscoveredServer(out, name, cfgRaw, source, configPath) {
125
+ const cfg = (cfgRaw || {});
126
+ const command = typeof cfg.command === 'string' ? cfg.command : undefined;
127
+ const args = Array.isArray(cfg.args) ? cfg.args.map(String) : undefined;
128
+ const url = typeof cfg.url === 'string' ? cfg.url : (typeof cfg.serverUrl === 'string' ? cfg.serverUrl : undefined);
129
+ const env = (cfg.env && typeof cfg.env === 'object') ? cfg.env : {};
130
+ const envKeys = Object.keys(env);
131
+ const transport = url
132
+ ? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
133
+ : command ? 'stdio' : 'unknown';
134
+ const tools = Array.isArray(cfg.tools)
135
+ ? cfg.tools
136
+ .map((t) => {
137
+ if (typeof t === 'string')
138
+ return { name: t };
139
+ if (t && typeof t.name === 'string')
140
+ return { name: t.name, description: typeof t.description === 'string' ? t.description : undefined };
141
+ return null;
142
+ })
143
+ .filter(Boolean)
144
+ : [];
145
+ const { level, tags } = scoreRisk({ serverName: name, command, args, url });
146
+ const warnings = [];
147
+ const blob = `${command || ''} ${(args || []).join(' ')} ${JSON.stringify(env)}`;
148
+ if (SECRET_VALUE.test(blob))
149
+ warnings.push('Hardcoded secret/API token detected in config (rotate + use env var).');
150
+ for (const [k, v] of Object.entries(env)) {
151
+ const val = String(v ?? '');
152
+ if (SECRET_KEY.test(k) && val && !/^\$\{?[A-Za-z0-9_]+\}?$/.test(val)) {
153
+ warnings.push(`Env "${k}" appears to contain a literal secret value.`);
154
+ }
155
+ }
156
+ out.push({
157
+ serverName: name,
158
+ command,
159
+ args,
160
+ url,
161
+ transport,
162
+ envKeys,
163
+ tools,
164
+ source,
165
+ configPath,
166
+ riskLevel: level,
167
+ riskTags: tags,
168
+ warnings,
169
+ probeMode: 'config',
170
+ proxyStatus: 'unknown',
171
+ });
172
+ }
106
173
  function parseConfigFile(filePath, source) {
107
174
  let raw;
108
175
  try {
@@ -118,56 +185,21 @@ function parseConfigFile(filePath, source) {
118
185
  catch {
119
186
  return [];
120
187
  }
121
- const servers = extractServerMap(parsed);
122
188
  const out = [];
123
- for (const [name, cfgRaw] of Object.entries(servers)) {
124
- const cfg = (cfgRaw || {});
125
- const command = typeof cfg.command === 'string' ? cfg.command : undefined;
126
- const args = Array.isArray(cfg.args) ? cfg.args.map(String) : undefined;
127
- const url = typeof cfg.url === 'string' ? cfg.url : (typeof cfg.serverUrl === 'string' ? cfg.serverUrl : undefined);
128
- const env = (cfg.env && typeof cfg.env === 'object') ? cfg.env : {};
129
- const envKeys = Object.keys(env);
130
- const transport = url
131
- ? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
132
- : command ? 'stdio' : 'unknown';
133
- const tools = Array.isArray(cfg.tools)
134
- ? cfg.tools
135
- .map((t) => {
136
- if (typeof t === 'string')
137
- return { name: t };
138
- if (t && typeof t.name === 'string')
139
- return { name: t.name, description: typeof t.description === 'string' ? t.description : undefined };
140
- return null;
141
- })
142
- .filter(Boolean)
143
- : [];
144
- const { level, tags } = scoreRisk({ serverName: name, command, args, url });
145
- const warnings = [];
146
- const blob = `${command || ''} ${(args || []).join(' ')} ${JSON.stringify(env)}`;
147
- if (SECRET_VALUE.test(blob))
148
- warnings.push('Hardcoded secret/API token detected in config (rotate + use env var).');
149
- for (const [k, v] of Object.entries(env)) {
150
- const val = String(v ?? '');
151
- if (SECRET_KEY.test(k) && val && !/^\$\{?[A-Za-z0-9_]+\}?$/.test(val)) {
152
- warnings.push(`Env "${k}" appears to contain a literal secret value.`);
189
+ for (const [name, cfgRaw] of Object.entries(extractServerMap(parsed))) {
190
+ pushDiscoveredServer(out, name, cfgRaw, source, filePath);
191
+ }
192
+ // Claude Code stores MCP per-project inside ~/.claude.json
193
+ if (/claude code/i.test(source) && parsed?.projects && typeof parsed.projects === 'object') {
194
+ for (const [projectPath, projectCfg] of Object.entries(parsed.projects)) {
195
+ const projectServers = projectCfg?.mcpServers;
196
+ if (!projectServers || typeof projectServers !== 'object')
197
+ continue;
198
+ const projectLabel = projectPath.split(/[/\\]/).filter(Boolean).slice(-1)[0] || projectPath;
199
+ for (const [name, cfgRaw] of Object.entries(projectServers)) {
200
+ pushDiscoveredServer(out, name, cfgRaw, `${source} · ${projectLabel}`, filePath);
153
201
  }
154
202
  }
155
- out.push({
156
- serverName: name,
157
- command,
158
- args,
159
- url,
160
- transport,
161
- envKeys,
162
- tools,
163
- source,
164
- configPath: filePath,
165
- riskLevel: level,
166
- riskTags: tags,
167
- warnings,
168
- probeMode: 'config',
169
- proxyStatus: 'unknown',
170
- });
171
203
  }
172
204
  return out;
173
205
  }
@@ -256,13 +288,14 @@ async function enrichWithDeepProbe(servers, silent) {
256
288
  server.probeMode = 'deep';
257
289
  }
258
290
  }
259
- /** Merge servers found in multiple files; keep richest record, union sources. */
291
+ /** Dedupe within the same config file + server name only (keep separate entries per client config path). */
260
292
  function dedupe(servers) {
261
- const byName = new Map();
293
+ const byKey = new Map();
262
294
  for (const s of servers) {
263
- const existing = byName.get(s.serverName);
295
+ const key = `${s.configPath.toLowerCase()}::${s.serverName.toLowerCase()}`;
296
+ const existing = byKey.get(key);
264
297
  if (!existing) {
265
- byName.set(s.serverName, { ...s, source: s.source });
298
+ byKey.set(key, { ...s });
266
299
  continue;
267
300
  }
268
301
  if (!existing.source.includes(s.source))
@@ -277,7 +310,7 @@ function dedupe(servers) {
277
310
  if (s.probeMode === 'deep')
278
311
  existing.probeMode = 'deep';
279
312
  }
280
- return [...byName.values()];
313
+ return [...byKey.values()];
281
314
  }
282
315
  const COLOR = {
283
316
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
@@ -318,11 +351,50 @@ function proxyStatusLabel(status) {
318
351
  return 'Direct — needs gateway';
319
352
  return 'Unknown';
320
353
  }
321
- async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorName) {
354
+ function sanitizeSecretForUpload(finding) {
355
+ return {
356
+ id: finding.id,
357
+ category: finding.category,
358
+ severity: finding.severity,
359
+ provider: finding.provider,
360
+ filePath: finding.filePath,
361
+ lineNumber: finding.lineNumber,
362
+ title: finding.title,
363
+ detail: finding.detail,
364
+ redactedPreview: finding.redactedPreview,
365
+ remediation: finding.remediation,
366
+ };
367
+ }
368
+ function sanitizeAgentFileForUpload(finding) {
369
+ return {
370
+ id: finding.id,
371
+ kind: finding.kind,
372
+ client: finding.client,
373
+ filePath: finding.filePath,
374
+ title: finding.title,
375
+ sizeBytes: finding.sizeBytes,
376
+ lineCount: finding.lineCount,
377
+ riskTags: finding.riskTags,
378
+ excerpt: finding.excerpt,
379
+ };
380
+ }
381
+ async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorName, extras) {
322
382
  const payload = {
323
383
  connectorName: connectorName || `Desktop MCP · ${host.hostname}`,
324
384
  host,
325
385
  clientCoverage,
386
+ posture: extras?.posture ? {
387
+ score: extras.posture.score,
388
+ grade: extras.posture.grade,
389
+ drivers: extras.posture.drivers,
390
+ combos: extras.posture.combos,
391
+ secretsHealthScore: extras.secrets?.healthScore,
392
+ secretsCritical: extras.secrets?.findings.filter(f => f.severity === 'critical').length ?? 0,
393
+ secretsHigh: extras.secrets?.findings.filter(f => f.severity === 'high').length ?? 0,
394
+ agentFilesCount: extras.agentFiles?.length ?? 0,
395
+ } : undefined,
396
+ secrets: extras?.secrets?.findings.slice(0, 100).map(sanitizeSecretForUpload),
397
+ agentFiles: extras?.agentFiles?.slice(0, 100).map(sanitizeAgentFileForUpload),
326
398
  servers: servers.map(s => ({
327
399
  serverName: s.serverName,
328
400
  command: s.command,
@@ -351,10 +423,71 @@ async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorNa
351
423
  throw new Error(data.error || `Upload failed (${resp.status})`);
352
424
  }
353
425
  const ingested = data.data?.ingested ?? servers.length;
354
- console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) from ${host.hostname} to your AI Inventory.${COLOR.reset}`);
426
+ const postureNote = extras?.posture ? ` · machine score ${extras.posture.score}/100 (${extras.posture.grade})` : '';
427
+ console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) from ${host.hostname} to your AI Inventory${postureNote}.${COLOR.reset}`);
428
+ }
429
+ function printSecretsReport(secrets, silent) {
430
+ if (silent)
431
+ return;
432
+ console.log(`\n${COLOR.bold}${COLOR.cyan}Secrets & exposure${COLOR.reset}`);
433
+ console.log(`${COLOR.gray}Scanned ${secrets.scannedFiles} file(s) and ${secrets.scannedStores} credential store(s). Health ${secrets.healthScore}/100.${COLOR.reset}\n`);
434
+ if (secrets.findings.length === 0) {
435
+ console.log(`${COLOR.green}No obvious leaked secrets in scanned locations.${COLOR.reset}`);
436
+ return;
437
+ }
438
+ for (const f of secrets.findings.slice(0, 40)) {
439
+ const col = f.severity === 'critical' || f.severity === 'high' ? COLOR.red : f.severity === 'medium' ? COLOR.yellow : COLOR.gray;
440
+ console.log(` ${col}[${f.severity.toUpperCase()}]${COLOR.reset} ${COLOR.bold}${f.title}${COLOR.reset} ${COLOR.gray}(${f.category})${COLOR.reset}`);
441
+ console.log(` ${COLOR.gray}path:${COLOR.reset} ${f.filePath}${f.lineNumber ? `:${f.lineNumber}` : ''}`);
442
+ if (f.redactedPreview)
443
+ console.log(` ${COLOR.gray}preview:${COLOR.reset} ${f.redactedPreview}`);
444
+ console.log(` ${COLOR.gray}fix:${COLOR.reset} ${f.remediation}`);
445
+ console.log('');
446
+ }
447
+ if (secrets.findings.length > 40) {
448
+ console.log(`${COLOR.gray}… and ${secrets.findings.length - 40} more (use --json for full list).${COLOR.reset}`);
449
+ }
450
+ }
451
+ function printAgentFilesReport(files, silent) {
452
+ if (silent)
453
+ return;
454
+ console.log(`\n${COLOR.bold}${COLOR.cyan}Agent files (rules / skills / instructions)${COLOR.reset}`);
455
+ console.log(`${COLOR.gray}Found ${files.length} file(s) that shape agent behavior on this machine.${COLOR.reset}\n`);
456
+ if (files.length === 0) {
457
+ console.log(`${COLOR.yellow}No Cursor rules, AGENTS.md, CLAUDE.md, or skill files found in home or project.${COLOR.reset}`);
458
+ return;
459
+ }
460
+ for (const f of files.slice(0, 30)) {
461
+ console.log(` ${COLOR.bold}${f.title}${COLOR.reset} ${COLOR.gray}(${f.kind}${f.client ? ` · ${f.client}` : ''})${COLOR.reset}`);
462
+ console.log(` ${COLOR.gray}path:${COLOR.reset} ${f.filePath} · ${f.lineCount} lines`);
463
+ if (f.riskTags.length)
464
+ console.log(` ${COLOR.red}tags:${COLOR.reset} ${f.riskTags.join(', ')}`);
465
+ if (f.excerpt)
466
+ console.log(` ${COLOR.gray}excerpt:${COLOR.reset} ${f.excerpt}${f.excerpt.length >= 180 ? '…' : ''}`);
467
+ console.log('');
468
+ }
469
+ }
470
+ function printPostureReport(posture, silent) {
471
+ if (silent)
472
+ return;
473
+ const col = posture.score >= 80 ? COLOR.green : posture.score >= 60 ? COLOR.yellow : COLOR.red;
474
+ console.log(`\n${COLOR.bold}${COLOR.cyan}Machine blast radius${COLOR.reset}`);
475
+ console.log(`${col}${COLOR.bold}Score ${posture.score}/100 · Grade ${posture.grade}${COLOR.reset}`);
476
+ if (posture.drivers.length)
477
+ console.log(`${COLOR.gray}Drivers: ${posture.drivers.join(' · ')}${COLOR.reset}`);
478
+ if (posture.combos.length === 0) {
479
+ console.log(`${COLOR.green}No dangerous MCP combos detected.${COLOR.reset}`);
480
+ return;
481
+ }
482
+ console.log('');
483
+ for (const combo of posture.combos) {
484
+ const c = combo.severity === 'critical' ? COLOR.red : combo.severity === 'high' ? COLOR.yellow : COLOR.gray;
485
+ console.log(` ${c}[${combo.severity.toUpperCase()}]${COLOR.reset} ${COLOR.bold}${combo.title}${COLOR.reset}`);
486
+ console.log(` ${combo.detail}`);
487
+ }
355
488
  }
356
489
  async function discoverCommand(args, config) {
357
- const type = (args.type || 'mcp').toLowerCase();
490
+ const surfaces = parseSurfaces(args);
358
491
  const silent = args.silent === 'true';
359
492
  const deep = args.deep === 'true';
360
493
  if (args.unschedule === 'true') {
@@ -372,141 +505,139 @@ async function discoverCommand(args, config) {
372
505
  }
373
506
  return;
374
507
  }
375
- if (type !== 'mcp') {
376
- if (!silent)
377
- console.error(`Unsupported discover type "${type}". Supported: mcp`);
378
- process.exit(1);
379
- }
380
508
  const cwd = process.cwd();
381
- const candidates = candidateConfigPaths(cwd, args.extraPath);
509
+ const runMcp = surfaces.has('mcp');
382
510
  const scanned = [];
383
511
  let found = [];
384
- for (const c of candidates) {
385
- scanned.push({ path: c.path, source: c.source });
386
- if (fs.existsSync(c.path)) {
387
- found.push(...parseConfigFile(c.path, c.source));
512
+ let clientCoverage = [];
513
+ if (runMcp) {
514
+ const candidates = candidateConfigPaths(cwd, args.extraPath);
515
+ for (const c of candidates) {
516
+ scanned.push({ path: c.path, source: c.source });
517
+ if (fs.existsSync(c.path)) {
518
+ found.push(...parseConfigFile(c.path, c.source));
519
+ }
520
+ }
521
+ found = dedupe(found);
522
+ clientCoverage = applyProxyClassification(found, scanned, cwd);
523
+ if (deep && found.length > 0) {
524
+ if (!silent)
525
+ console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
526
+ await enrichWithDeepProbe(found, silent);
388
527
  }
389
528
  }
390
- found = dedupe(found);
391
- const clientCoverage = applyProxyClassification(found, scanned, cwd);
392
- if (deep && found.length > 0) {
393
- if (!silent)
394
- console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
395
- await enrichWithDeepProbe(found, silent);
396
- }
529
+ const secrets = surfaces.has('secrets') || surfaces.has('posture')
530
+ ? (0, discoverSecrets_1.scanSecrets)({ cwd })
531
+ : undefined;
532
+ const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
533
+ ? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd })
534
+ : undefined;
535
+ const posture = surfaces.has('posture')
536
+ ? (0, discoverBlastRadius_1.computeBlastRadius)({
537
+ mcpServers: found,
538
+ clientCoverage,
539
+ secrets: secrets || { findings: [], scannedFiles: 0, scannedStores: 0, healthScore: 100 },
540
+ agentFiles: agentFiles || [],
541
+ })
542
+ : undefined;
397
543
  const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
398
544
  if (args.json === 'true') {
399
545
  console.log(JSON.stringify({
400
546
  host,
547
+ surfaces: [...surfaces],
401
548
  scannedFiles: scanned.map(s => `${s.source} → ${s.path}`),
402
549
  clientCoverage,
403
550
  servers: found,
551
+ secrets,
552
+ agentFiles,
553
+ posture,
404
554
  }, null, 2));
405
555
  return;
406
556
  }
557
+ const uploadExtras = { secrets, agentFiles, posture };
558
+ async function maybeUpload() {
559
+ if (args.upload !== 'true')
560
+ return;
561
+ const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
562
+ if (!creds.apiKey) {
563
+ if (!silent) {
564
+ console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset} Run \`fullcourtdefense setup\` first.`);
565
+ }
566
+ process.exit(1);
567
+ }
568
+ await upload(found, host, clientCoverage, creds.apiUrl, creds.apiKey, args.connectorName, uploadExtras);
569
+ }
407
570
  if (silent) {
408
571
  if (args.upload === 'true') {
409
- const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
410
- const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
411
- if (!apiKey)
572
+ const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
573
+ if (!creds.apiKey)
412
574
  process.exit(1);
413
- if (scanned.length === 0)
575
+ if (!runMcp && !secrets && !agentFiles && !posture)
414
576
  return;
415
- await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
577
+ await maybeUpload();
416
578
  }
417
579
  return;
418
580
  }
419
- console.log(`\n${COLOR.bold}${COLOR.cyan}MCP server discovery${COLOR.reset}`);
420
- console.log(`${COLOR.gray}Host ${host.hostname} · ${host.platform} · machine ${host.machineId}${COLOR.reset}`);
421
- console.log(`${COLOR.gray}Scanned ${candidates.length} known config locations on this machine.${COLOR.reset}\n`);
422
- if (scanned.length === 0) {
423
- console.log(`${COLOR.yellow}No MCP config files found.${COLOR.reset}`);
424
- console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop/Code, VS Code, Windsurf, Codex, Gemini CLI, and repo-root configs.${COLOR.reset}`);
425
- return;
426
- }
427
- console.log(`${COLOR.bold}AI clients checked (${scanned.length}):${COLOR.reset}`);
428
- for (const s of scanned) {
429
- const exists = fs.existsSync(s.path);
430
- console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} → ${s.path}${exists ? '' : ` ${COLOR.yellow}(not created yet)${COLOR.reset}`}`);
431
- }
432
- console.log('');
433
- if (clientCoverage.length > 0) {
434
- console.log(`${COLOR.bold}Client coverage:${COLOR.reset}`);
435
- for (const row of clientCoverage) {
436
- const gateway = row.mcpGatewayInstalled ? `${COLOR.green}gateway ✓${COLOR.reset}` : `${COLOR.red}no gateway${COLOR.reset}`;
437
- const hooks = row.clientKey === 'cursor'
438
- ? (row.cursorHooksInstalled ? `${COLOR.green}hooks ✓ (${row.cursorHookEvents.join(', ')})${COLOR.reset}` : `${COLOR.yellow}no hooks${COLOR.reset}`)
439
- : '';
440
- console.log(` ${COLOR.dim}•${COLOR.reset} ${row.client} — ${row.mcpServerCount} MCP · ${gateway}${hooks ? ` · ${hooks}` : ''}`);
441
- if (row.clientKey === 'claude_desktop' && !row.mcpGatewayInstalled && row.mcpServerCount === 0) {
442
- console.log(` ${COLOR.gray}Install: fullcourtdefense install-claude-desktop-mcp-gateway --shield-id ... --mcp-command npm --mcp-args "run mcp"${COLOR.reset}`);
443
- }
581
+ const title = surfaces.size === 1 && surfaces.has('mcp')
582
+ ? 'MCP server discovery'
583
+ : `Desktop AI posture (${[...surfaces].join(', ')})`;
584
+ console.log(`\n${COLOR.bold}${COLOR.cyan}${title}${COLOR.reset}`);
585
+ console.log(`${COLOR.gray}Host ${host.hostname} · ${host.platform} · machine ${host.machineId}${COLOR.reset}\n`);
586
+ if (runMcp) {
587
+ console.log(`${COLOR.bold}MCP inventory${COLOR.reset}`);
588
+ if (scanned.length === 0) {
589
+ console.log(`${COLOR.yellow}No MCP config files found.${COLOR.reset}\n`);
444
590
  }
445
- console.log('');
446
- }
447
- if (found.length === 0) {
448
- console.log(`${COLOR.yellow}No MCP servers configured in these clients yet.${COLOR.reset}`);
449
- console.log(`${COLOR.gray}Install gateway everywhere: fullcourtdefense install-mcp-gateway --clients all --shield-id ... --shield-key ... --mcp-command npm --mcp-args "run mcp"${COLOR.reset}`);
450
- if (args.upload === 'true') {
451
- const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
452
- const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
453
- if (!apiKey) {
454
- console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset}`);
455
- process.exit(1);
591
+ else {
592
+ console.log(`${COLOR.gray}Scanned ${scanned.length} known config locations.${COLOR.reset}\n`);
593
+ for (const s of scanned) {
594
+ const exists = fs.existsSync(s.path);
595
+ console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} ${s.path}${exists ? '' : ` ${COLOR.yellow}(not created yet)${COLOR.reset}`}`);
456
596
  }
457
597
  console.log('');
458
- await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
459
- }
460
- else {
461
- console.log(`${COLOR.gray}Run with --upload to push client coverage into AI Inventory.${COLOR.reset}`);
462
- }
463
- return;
464
- }
465
- const order = { critical: 0, high: 1, medium: 2, low: 3 };
466
- found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
467
- console.log(`${COLOR.bold}${found.length} MCP server(s) discovered:${COLOR.reset}\n`);
468
- for (const s of found) {
469
- const col = riskColor(s.riskLevel);
470
- const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
471
- const proxyCol = s.proxyStatus === 'direct' ? COLOR.red : s.proxyStatus === 'proxied' || s.proxyStatus === 'fcd_gateway' ? COLOR.green : COLOR.yellow;
472
- 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}`);
473
- if (s.serverDescription)
474
- console.log(` ${COLOR.gray}about:${COLOR.reset} ${s.serverDescription}`);
475
- console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
476
- if (s.riskTags.length)
477
- console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
478
- if (s.tools.length) {
479
- const names = s.tools.map(t => t.name);
480
- console.log(` ${COLOR.gray}tools:${COLOR.reset} ${names.slice(0, 12).join(', ')}${names.length > 12 ? ' …' : ''}`);
598
+ if (clientCoverage.length > 0) {
599
+ for (const row of clientCoverage) {
600
+ const gateway = row.mcpGatewayInstalled ? `${COLOR.green}gateway ✓${COLOR.reset}` : `${COLOR.red}no gateway${COLOR.reset}`;
601
+ console.log(` ${COLOR.dim}•${COLOR.reset} ${row.client} ${row.mcpServerCount} MCP · ${gateway}`);
602
+ }
603
+ console.log('');
604
+ }
605
+ if (found.length > 0) {
606
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
607
+ found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
608
+ for (const s of found) {
609
+ const col = riskColor(s.riskLevel);
610
+ const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
611
+ console.log(` ${col}[${s.riskLevel.toUpperCase()}]${COLOR.reset} ${s.serverName} ${target} (${proxyStatusLabel(s.proxyStatus)})`);
612
+ }
613
+ console.log('');
614
+ }
481
615
  }
482
- console.log(` ${COLOR.gray}from:${COLOR.reset} ${s.source}`);
483
- for (const w of s.warnings)
484
- console.log(` ${COLOR.red}⚠ ${w}${COLOR.reset}`);
485
- console.log('');
486
616
  }
487
- const counts = found.reduce((acc, s) => { acc[s.riskLevel] = (acc[s.riskLevel] || 0) + 1; return acc; }, {});
488
- console.log(`${COLOR.bold}Summary:${COLOR.reset} ${counts.critical || 0} critical · ${counts.high || 0} high · ${counts.medium || 0} medium · ${counts.low || 0} low`);
617
+ if (secrets)
618
+ printSecretsReport(secrets, silent);
619
+ if (agentFiles)
620
+ printAgentFilesReport(agentFiles, silent);
621
+ if (posture)
622
+ printPostureReport(posture, silent);
489
623
  if (args.upload === 'true') {
490
- const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
491
- const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
492
- if (!apiKey) {
493
- console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset} Set --api-key, FULLCOURTDEFENSE_API_KEY, or apiKey in config.`);
494
- process.exit(1);
495
- }
496
624
  console.log('');
497
- await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
625
+ await maybeUpload();
498
626
  }
499
627
  else {
500
- console.log(`${COLOR.gray}Run with --upload to push these into your AI Inventory.${COLOR.reset}`);
501
- if (!deep)
502
- console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE servers for tools/list.${COLOR.reset}`);
503
- console.log(`${COLOR.gray}Add --schedule daily for automatic daily upload (MDM/fleet).${COLOR.reset}`);
628
+ console.log(`${COLOR.gray}Run with --upload to push results into AI Fleet.${COLOR.reset}`);
629
+ if (runMcp && !deep)
630
+ console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE MCP servers.${COLOR.reset}`);
631
+ if (!surfaces.has('secrets'))
632
+ console.log(`${COLOR.gray}Add --surface secrets for .env / credential store scan (local-only).${COLOR.reset}`);
633
+ if (surfaces.size < 4)
634
+ console.log(`${COLOR.gray}Add --surface all for MCP + secrets + agent files + blast radius.${COLOR.reset}`);
504
635
  }
505
636
  }
506
637
  /** Upload-only discover — used after install-mcp-gateway --upload. */
507
638
  async function runDiscoverUpload(args, config) {
508
639
  await discoverCommand({
509
- type: 'mcp',
640
+ surface: 'all',
510
641
  upload: 'true',
511
642
  silent: args.silent ? 'true' : undefined,
512
643
  apiKey: args.apiKey,
@@ -0,0 +1,15 @@
1
+ export type AgentFileKind = 'rules' | 'skills' | 'instructions' | 'hooks' | 'memory' | 'other';
2
+ export interface AgentFileFinding {
3
+ id: string;
4
+ kind: AgentFileKind;
5
+ client?: string;
6
+ filePath: string;
7
+ title: string;
8
+ sizeBytes: number;
9
+ lineCount: number;
10
+ riskTags: string[];
11
+ excerpt?: string;
12
+ }
13
+ export declare function scanAgentFiles(options?: {
14
+ cwd?: string;
15
+ }): AgentFileFinding[];