fullcourtdefense-cli 1.3.1 → 1.3.3

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/api.d.ts CHANGED
@@ -91,6 +91,14 @@ export interface ApiError {
91
91
  code?: string;
92
92
  remaining?: number;
93
93
  }
94
+ export interface SetupValidationResponse {
95
+ organizationId: string;
96
+ organizationName: string;
97
+ shieldId: string;
98
+ shieldName: string;
99
+ shieldKeyRequired: boolean;
100
+ gatewayMode: boolean;
101
+ }
94
102
  export declare class BotGuardApi {
95
103
  private apiUrl;
96
104
  private apiKey;
@@ -100,4 +108,9 @@ export declare class BotGuardApi {
100
108
  getScanStatus(jobId: string): Promise<AsyncJobResponse>;
101
109
  pollUntilDone(jobId: string, intervalMs?: number, maxWaitMs?: number): Promise<ScanResult>;
102
110
  getCredits(): Promise<CreditsResponse>;
111
+ validateSetup(input: {
112
+ organizationId?: string;
113
+ shieldId: string;
114
+ shieldKey?: string;
115
+ }): Promise<SetupValidationResponse>;
103
116
  }
package/dist/api.js CHANGED
@@ -19,10 +19,26 @@ class BotGuardApi {
19
19
  },
20
20
  body: body ? JSON.stringify(body) : undefined,
21
21
  });
22
- const data = await resp.json();
22
+ const contentType = resp.headers.get('content-type') || '';
23
+ const raw = await resp.text();
24
+ let data;
25
+ try {
26
+ data = raw ? JSON.parse(raw) : {};
27
+ }
28
+ catch {
29
+ if (raw.trimStart().startsWith('<!DOCTYPE') || raw.trimStart().startsWith('<html')) {
30
+ throw new Error(`API returned HTML instead of JSON (${resp.status} ${path}). `
31
+ + 'The endpoint may not be deployed yet — retry with `fullcourtdefense setup --skip-validate true` '
32
+ + 'or confirm https://api.fullcourtdefense.ai is reachable.');
33
+ }
34
+ throw new Error(`Invalid API response (${resp.status} ${path}): ${raw.slice(0, 200)}`);
35
+ }
36
+ if (!contentType.includes('json') && !data?.success && !resp.ok) {
37
+ // Some gateways omit content-type; JSON parse already succeeded.
38
+ }
23
39
  if (!resp.ok || data.success === false) {
24
40
  const err = data;
25
- const msg = err.error || `API error (${resp.status})`;
41
+ const msg = err.code ? `${err.error} (${err.code})` : (err.error || `API error (${resp.status})`);
26
42
  if (err.code === 'CREDITS_EXHAUSTED') {
27
43
  throw new Error(`No scan credits remaining this month. Upgrade your plan at https://fullcourtdefense.ai/pricing`);
28
44
  }
@@ -58,5 +74,8 @@ class BotGuardApi {
58
74
  async getCredits() {
59
75
  return this.request('GET', '/api/cicd/credits');
60
76
  }
77
+ async validateSetup(input) {
78
+ return this.request('POST', '/api/cicd/setup/validate', input);
79
+ }
61
80
  }
62
81
  exports.BotGuardApi = BotGuardApi;
@@ -1,7 +1,10 @@
1
1
  import { BotGuardConfig } from '../config';
2
2
  export interface ConfigureArgs {
3
+ apiKey?: string;
4
+ organizationId?: string;
3
5
  shieldId?: string;
4
6
  shieldKey?: string;
5
7
  apiUrl?: string;
8
+ skipValidate?: boolean;
6
9
  }
7
10
  export declare function configureCommand(args: ConfigureArgs, config: BotGuardConfig): Promise<void>;
@@ -36,25 +36,70 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.configureCommand = configureCommand;
37
37
  const readline = __importStar(require("readline/promises"));
38
38
  const process_1 = require("process");
39
+ const api_1 = require("../api");
39
40
  const config_1 = require("../config");
40
41
  async function ask(rl, question, fallback) {
41
42
  const suffix = fallback ? ` (${fallback})` : '';
42
43
  const answer = (await rl.question(`${question}${suffix}: `)).trim();
43
44
  return answer || fallback || '';
44
45
  }
46
+ function resolveApiKey(args, config) {
47
+ return args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY || '';
48
+ }
45
49
  async function configureCommand(args, config) {
46
- const rl = readline.createInterface({ input: process_1.stdin, output: process_1.stdout });
47
- try {
48
- const shieldId = args.shieldId || await ask(rl, 'Shield ID', config.shieldId || 'sh_...');
49
- if (!shieldId || shieldId === 'sh_...') {
50
- throw new Error('Shield ID is required.');
50
+ const apiUrl = args.apiUrl || config.apiUrl || 'https://api.fullcourtdefense.ai';
51
+ const apiKey = resolveApiKey(args, config);
52
+ const organizationId = args.organizationId || config.organizationId;
53
+ const shieldId = args.shieldId || config.shieldId;
54
+ const shieldKey = args.shieldKey ?? config.shieldKey;
55
+ const hasAllRequired = Boolean(apiKey && organizationId && shieldId);
56
+ let finalApiUrl = apiUrl;
57
+ let finalApiKey = apiKey;
58
+ let finalOrgId = organizationId;
59
+ let finalShieldId = shieldId;
60
+ let finalShieldKey = shieldKey;
61
+ if (!hasAllRequired) {
62
+ const rl = readline.createInterface({ input: process_1.stdin, output: process_1.stdout });
63
+ try {
64
+ finalApiUrl = args.apiUrl || await ask(rl, 'FullCourtDefense API URL', config.apiUrl || 'https://api.fullcourtdefense.ai');
65
+ finalApiKey = resolveApiKey(args, config) || await ask(rl, 'Organization API key (from Settings → API Keys)', '');
66
+ finalOrgId = args.organizationId || await ask(rl, 'Organization ID (from Settings → Organization)', config.organizationId || '');
67
+ finalShieldId = args.shieldId || await ask(rl, 'Shield ID', config.shieldId || 'sh_...');
68
+ finalShieldKey = args.shieldKey ?? await ask(rl, 'Shield key if locked, blank if not required', config.shieldKey || '');
69
+ }
70
+ finally {
71
+ rl.close();
51
72
  }
52
- const shieldKey = args.shieldKey || await ask(rl, 'Shield key if locked, blank if not required', config.shieldKey || '');
53
- const apiUrl = args.apiUrl || await ask(rl, 'FullCourtDefense API URL', config.apiUrl || 'https://api.fullcourtdefense.ai');
54
- const savedPath = (0, config_1.saveShieldConfig)({ shieldId, shieldKey: shieldKey || undefined, apiUrl });
55
- console.log(`Saved Shield config to ${savedPath}`);
56
73
  }
57
- finally {
58
- rl.close();
74
+ if (!finalApiKey) {
75
+ throw new Error('Organization API key is required to validate your setup.');
76
+ }
77
+ if (!finalOrgId) {
78
+ throw new Error('Organization ID is required.');
79
+ }
80
+ if (!finalShieldId || finalShieldId === 'sh_...') {
81
+ throw new Error('Shield ID is required.');
82
+ }
83
+ if (args.skipValidate !== true) {
84
+ console.log('Validating organization, Shield ID, and Shield key against FullCourtDefense...');
85
+ const api = new api_1.BotGuardApi(finalApiKey, finalApiUrl);
86
+ const validated = await api.validateSetup({
87
+ organizationId: finalOrgId,
88
+ shieldId: finalShieldId,
89
+ shieldKey: finalShieldKey || undefined,
90
+ });
91
+ if (validated.organizationId !== finalOrgId) {
92
+ throw new Error(`Organization mismatch: API key belongs to "${validated.organizationName}" (${validated.organizationId}), not "${finalOrgId}".`);
93
+ }
94
+ console.log(`Verified: ${validated.organizationName} → Shield "${validated.shieldName}" (${validated.shieldId})`);
59
95
  }
96
+ const savedPath = (0, config_1.saveSetupConfig)({
97
+ apiKey: finalApiKey,
98
+ organizationId: finalOrgId,
99
+ shieldId: finalShieldId,
100
+ shieldKey: finalShieldKey || undefined,
101
+ apiUrl: finalApiUrl,
102
+ });
103
+ console.log(`Saved setup to ${savedPath}`);
104
+ console.log('All other CLI commands (install, discover --upload, scan, hooks) will use these credentials automatically.');
60
105
  }
@@ -1,6 +1,8 @@
1
1
  import { BotGuardConfig } from '../config';
2
+ export type DiscoverSurface = 'mcp' | 'secrets' | 'agent-files' | 'posture';
2
3
  export interface DiscoverArgs {
3
4
  type?: string;
5
+ surface?: string;
4
6
  apiKey?: string;
5
7
  apiUrl?: string;
6
8
  json?: string;
@@ -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() {
@@ -333,11 +351,50 @@ function proxyStatusLabel(status) {
333
351
  return 'Direct — needs gateway';
334
352
  return 'Unknown';
335
353
  }
336
- 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) {
337
382
  const payload = {
338
383
  connectorName: connectorName || `Desktop MCP · ${host.hostname}`,
339
384
  host,
340
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),
341
398
  servers: servers.map(s => ({
342
399
  serverName: s.serverName,
343
400
  command: s.command,
@@ -366,10 +423,71 @@ async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorNa
366
423
  throw new Error(data.error || `Upload failed (${resp.status})`);
367
424
  }
368
425
  const ingested = data.data?.ingested ?? servers.length;
369
- 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
+ }
370
488
  }
371
489
  async function discoverCommand(args, config) {
372
- const type = (args.type || 'mcp').toLowerCase();
490
+ const surfaces = parseSurfaces(args);
373
491
  const silent = args.silent === 'true';
374
492
  const deep = args.deep === 'true';
375
493
  if (args.unschedule === 'true') {
@@ -387,141 +505,139 @@ async function discoverCommand(args, config) {
387
505
  }
388
506
  return;
389
507
  }
390
- if (type !== 'mcp') {
391
- if (!silent)
392
- console.error(`Unsupported discover type "${type}". Supported: mcp`);
393
- process.exit(1);
394
- }
395
508
  const cwd = process.cwd();
396
- const candidates = candidateConfigPaths(cwd, args.extraPath);
509
+ const runMcp = surfaces.has('mcp');
397
510
  const scanned = [];
398
511
  let found = [];
399
- for (const c of candidates) {
400
- scanned.push({ path: c.path, source: c.source });
401
- if (fs.existsSync(c.path)) {
402
- 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);
403
527
  }
404
528
  }
405
- found = dedupe(found);
406
- const clientCoverage = applyProxyClassification(found, scanned, cwd);
407
- if (deep && found.length > 0) {
408
- if (!silent)
409
- console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
410
- await enrichWithDeepProbe(found, silent);
411
- }
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;
412
543
  const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
413
544
  if (args.json === 'true') {
414
545
  console.log(JSON.stringify({
415
546
  host,
547
+ surfaces: [...surfaces],
416
548
  scannedFiles: scanned.map(s => `${s.source} → ${s.path}`),
417
549
  clientCoverage,
418
550
  servers: found,
551
+ secrets,
552
+ agentFiles,
553
+ posture,
419
554
  }, null, 2));
420
555
  return;
421
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
+ }
422
570
  if (silent) {
423
571
  if (args.upload === 'true') {
424
- const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
425
- const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
426
- if (!apiKey)
572
+ const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
573
+ if (!creds.apiKey)
427
574
  process.exit(1);
428
- if (scanned.length === 0)
575
+ if (!runMcp && !secrets && !agentFiles && !posture)
429
576
  return;
430
- await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
577
+ await maybeUpload();
431
578
  }
432
579
  return;
433
580
  }
434
- console.log(`\n${COLOR.bold}${COLOR.cyan}MCP server discovery${COLOR.reset}`);
435
- console.log(`${COLOR.gray}Host ${host.hostname} · ${host.platform} · machine ${host.machineId}${COLOR.reset}`);
436
- console.log(`${COLOR.gray}Scanned ${candidates.length} known config locations on this machine.${COLOR.reset}\n`);
437
- if (scanned.length === 0) {
438
- console.log(`${COLOR.yellow}No MCP config files found.${COLOR.reset}`);
439
- console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop/Code, VS Code, Windsurf, Codex, Gemini CLI, and repo-root configs.${COLOR.reset}`);
440
- return;
441
- }
442
- console.log(`${COLOR.bold}AI clients checked (${scanned.length}):${COLOR.reset}`);
443
- for (const s of scanned) {
444
- const exists = fs.existsSync(s.path);
445
- console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} → ${s.path}${exists ? '' : ` ${COLOR.yellow}(not created yet)${COLOR.reset}`}`);
446
- }
447
- console.log('');
448
- if (clientCoverage.length > 0) {
449
- console.log(`${COLOR.bold}Client coverage:${COLOR.reset}`);
450
- for (const row of clientCoverage) {
451
- const gateway = row.mcpGatewayInstalled ? `${COLOR.green}gateway ✓${COLOR.reset}` : `${COLOR.red}no gateway${COLOR.reset}`;
452
- const hooks = row.clientKey === 'cursor'
453
- ? (row.cursorHooksInstalled ? `${COLOR.green}hooks ✓ (${row.cursorHookEvents.join(', ')})${COLOR.reset}` : `${COLOR.yellow}no hooks${COLOR.reset}`)
454
- : '';
455
- console.log(` ${COLOR.dim}•${COLOR.reset} ${row.client} — ${row.mcpServerCount} MCP · ${gateway}${hooks ? ` · ${hooks}` : ''}`);
456
- if (row.clientKey === 'claude_desktop' && !row.mcpGatewayInstalled && row.mcpServerCount === 0) {
457
- console.log(` ${COLOR.gray}Install: fullcourtdefense install-claude-desktop-mcp-gateway --shield-id ... --mcp-command npm --mcp-args "run mcp"${COLOR.reset}`);
458
- }
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`);
459
590
  }
460
- console.log('');
461
- }
462
- if (found.length === 0) {
463
- console.log(`${COLOR.yellow}No MCP servers configured in these clients yet.${COLOR.reset}`);
464
- 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}`);
465
- if (args.upload === 'true') {
466
- const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
467
- const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
468
- if (!apiKey) {
469
- console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset}`);
470
- 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}`}`);
471
596
  }
472
597
  console.log('');
473
- await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
474
- }
475
- else {
476
- console.log(`${COLOR.gray}Run with --upload to push client coverage into AI Inventory.${COLOR.reset}`);
477
- }
478
- return;
479
- }
480
- const order = { critical: 0, high: 1, medium: 2, low: 3 };
481
- found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
482
- console.log(`${COLOR.bold}${found.length} MCP server(s) discovered:${COLOR.reset}\n`);
483
- for (const s of found) {
484
- const col = riskColor(s.riskLevel);
485
- const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
486
- const proxyCol = s.proxyStatus === 'direct' ? COLOR.red : s.proxyStatus === 'proxied' || s.proxyStatus === 'fcd_gateway' ? COLOR.green : COLOR.yellow;
487
- 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}`);
488
- if (s.serverDescription)
489
- console.log(` ${COLOR.gray}about:${COLOR.reset} ${s.serverDescription}`);
490
- console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
491
- if (s.riskTags.length)
492
- console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
493
- if (s.tools.length) {
494
- const names = s.tools.map(t => t.name);
495
- 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
+ }
496
615
  }
497
- console.log(` ${COLOR.gray}from:${COLOR.reset} ${s.source}`);
498
- for (const w of s.warnings)
499
- console.log(` ${COLOR.red}⚠ ${w}${COLOR.reset}`);
500
- console.log('');
501
616
  }
502
- const counts = found.reduce((acc, s) => { acc[s.riskLevel] = (acc[s.riskLevel] || 0) + 1; return acc; }, {});
503
- 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);
504
623
  if (args.upload === 'true') {
505
- const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
506
- const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
507
- if (!apiKey) {
508
- console.error(`\n${COLOR.red}--upload requires an API key.${COLOR.reset} Set --api-key, FULLCOURTDEFENSE_API_KEY, or apiKey in config.`);
509
- process.exit(1);
510
- }
511
624
  console.log('');
512
- await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
625
+ await maybeUpload();
513
626
  }
514
627
  else {
515
- console.log(`${COLOR.gray}Run with --upload to push these into your AI Inventory.${COLOR.reset}`);
516
- if (!deep)
517
- console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE servers for tools/list.${COLOR.reset}`);
518
- 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}`);
519
635
  }
520
636
  }
521
637
  /** Upload-only discover — used after install-mcp-gateway --upload. */
522
638
  async function runDiscoverUpload(args, config) {
523
639
  await discoverCommand({
524
- type: 'mcp',
640
+ surface: 'all',
525
641
  upload: 'true',
526
642
  silent: args.silent ? 'true' : undefined,
527
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[];