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.
- package/dist/api.d.ts +13 -0
- package/dist/api.js +4 -1
- package/dist/commands/configure.d.ts +3 -0
- package/dist/commands/configure.js +36 -4
- package/dist/commands/discover.d.ts +2 -0
- package/dist/commands/discover.js +285 -154
- 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
|
@@ -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(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
/**
|
|
291
|
+
/** Dedupe within the same config file + server name only (keep separate entries per client config path). */
|
|
260
292
|
function dedupe(servers) {
|
|
261
|
-
const
|
|
293
|
+
const byKey = new Map();
|
|
262
294
|
for (const s of servers) {
|
|
263
|
-
const
|
|
295
|
+
const key = `${s.configPath.toLowerCase()}::${s.serverName.toLowerCase()}`;
|
|
296
|
+
const existing = byKey.get(key);
|
|
264
297
|
if (!existing) {
|
|
265
|
-
|
|
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 [...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
509
|
+
const runMcp = surfaces.has('mcp');
|
|
382
510
|
const scanned = [];
|
|
383
511
|
let found = [];
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
410
|
-
|
|
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 (
|
|
575
|
+
if (!runMcp && !secrets && !agentFiles && !posture)
|
|
414
576
|
return;
|
|
415
|
-
await
|
|
577
|
+
await maybeUpload();
|
|
416
578
|
}
|
|
417
579
|
return;
|
|
418
580
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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
|
|
625
|
+
await maybeUpload();
|
|
498
626
|
}
|
|
499
627
|
else {
|
|
500
|
-
console.log(`${COLOR.gray}Run with --upload to push
|
|
501
|
-
if (!deep)
|
|
502
|
-
console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE servers
|
|
503
|
-
|
|
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
|
-
|
|
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[];
|