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 +13 -0
- package/dist/api.js +21 -2
- package/dist/commands/configure.d.ts +3 -0
- package/dist/commands/configure.js +56 -11
- package/dist/commands/discover.d.ts +2 -0
- package/dist/commands/discover.js +218 -102
- package/dist/commands/discoverAgentFiles.d.ts +15 -0
- package/dist/commands/discoverAgentFiles.js +204 -0
- package/dist/commands/discoverBlastRadius.d.ts +30 -0
- package/dist/commands/discoverBlastRadius.js +139 -0
- package/dist/commands/discoverSecrets.d.ts +24 -0
- package/dist/commands/discoverSecrets.js +286 -0
- package/dist/commands/hook.js +9 -27
- package/dist/commands/installAll.d.ts +11 -0
- package/dist/commands/installAll.js +55 -0
- package/dist/commands/installCursorHook.js +16 -27
- package/dist/commands/mcpGateway.d.ts +23 -0
- package/dist/commands/mcpGateway.js +313 -67
- package/dist/config.d.ts +26 -0
- package/dist/config.js +92 -16
- package/dist/index.js +191 -120
- package/dist/version.json +3 -0
- package/package.json +1 -1
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
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
509
|
+
const runMcp = surfaces.has('mcp');
|
|
397
510
|
const scanned = [];
|
|
398
511
|
let found = [];
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
425
|
-
|
|
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 (
|
|
575
|
+
if (!runMcp && !secrets && !agentFiles && !posture)
|
|
429
576
|
return;
|
|
430
|
-
await
|
|
577
|
+
await maybeUpload();
|
|
431
578
|
}
|
|
432
579
|
return;
|
|
433
580
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
|
625
|
+
await maybeUpload();
|
|
513
626
|
}
|
|
514
627
|
else {
|
|
515
|
-
console.log(`${COLOR.gray}Run with --upload to push
|
|
516
|
-
if (!deep)
|
|
517
|
-
console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE servers
|
|
518
|
-
|
|
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
|
-
|
|
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[];
|