agentaudit 3.10.9 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +574 -162
- package/index.mjs +795 -659
- package/package.json +7 -3
- package/scan-tool-poisoning.mjs +297 -0
- package/tool-poisoning-detector.mjs +913 -0
package/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Usage: agentaudit <command> [options]
|
|
6
6
|
*
|
|
7
7
|
* Commands:
|
|
8
|
-
* discover Find MCP servers
|
|
8
|
+
* discover Find MCP servers across all AI tools
|
|
9
9
|
* scan <url> [url...] Quick static scan (regex)
|
|
10
10
|
* audit <url> [url...] Deep LLM-powered security audit
|
|
11
11
|
* lookup <name> Look up package in registry
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* model [name|reset] Configure LLM provider + model
|
|
18
18
|
* setup Log in to agentaudit.dev (for report uploads)
|
|
19
19
|
* status Show current config + auth status
|
|
20
|
+
* profile Your profile — rank, points, audit stats
|
|
20
21
|
* help [command] Show help
|
|
21
22
|
*
|
|
22
23
|
* Flags: --json, --quiet, --no-color, --no-upload, --model, --export, --debug
|
|
@@ -435,26 +436,31 @@ function singleSelect(items, { title = 'Select', hint = '↑↓=move Enter=sele
|
|
|
435
436
|
});
|
|
436
437
|
}
|
|
437
438
|
|
|
438
|
-
async function
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
439
|
+
async function validateApiKey(apiKey) {
|
|
440
|
+
try {
|
|
441
|
+
const res = await fetch(`${REGISTRY_URL}/api/auth/validate`, {
|
|
442
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
443
|
+
signal: AbortSignal.timeout(10_000),
|
|
444
|
+
});
|
|
445
|
+
if (res.ok) {
|
|
446
|
+
const data = await res.json();
|
|
447
|
+
return { valid: true, agent_name: data.agent_name || null };
|
|
448
|
+
}
|
|
449
|
+
return { valid: false, agent_name: null };
|
|
450
|
+
} catch {
|
|
451
|
+
return { valid: false, agent_name: null };
|
|
452
|
+
}
|
|
447
453
|
}
|
|
448
454
|
|
|
449
455
|
async function setupCommand() {
|
|
450
|
-
console.log(` ${c.bold}AgentAudit
|
|
451
|
-
console.log(` ${c.dim}
|
|
456
|
+
console.log(` ${c.bold}AgentAudit Setup${c.reset}`);
|
|
457
|
+
console.log(` ${c.dim}Link your API key to upload audit reports to agentaudit.dev${c.reset}`);
|
|
452
458
|
console.log();
|
|
453
459
|
|
|
454
460
|
const existing = loadCredentials();
|
|
455
461
|
if (existing) {
|
|
456
|
-
console.log(` ${icons.safe} Already
|
|
457
|
-
console.log(` ${c.dim}Key: ${existing.api_key.slice(0,
|
|
462
|
+
console.log(` ${icons.safe} Already configured as ${c.bold}${existing.agent_name}${c.reset}`);
|
|
463
|
+
console.log(` ${c.dim}Key: ${existing.api_key.slice(0, 12)}...${c.reset}`);
|
|
458
464
|
console.log();
|
|
459
465
|
const answer = await askQuestion(` Reconfigure? ${c.dim}(y/N)${c.reset} `);
|
|
460
466
|
if (answer.toLowerCase() !== 'y') {
|
|
@@ -464,39 +470,28 @@ async function setupCommand() {
|
|
|
464
470
|
console.log();
|
|
465
471
|
}
|
|
466
472
|
|
|
467
|
-
console.log(` ${c.bold}1
|
|
468
|
-
console.log(` ${c.
|
|
469
|
-
console.log();
|
|
470
|
-
const choice = await askQuestion(` Choice ${c.dim}(1/2)${c.reset}: `);
|
|
473
|
+
console.log(` ${c.bold}Step 1:${c.reset} Create an API key at ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
|
|
474
|
+
console.log(` ${c.dim}Sign in with GitHub, then click "Create API Key".${c.reset}`);
|
|
471
475
|
console.log();
|
|
476
|
+
const key = await askQuestion(` ${c.bold}Step 2:${c.reset} Paste your API key here: `);
|
|
477
|
+
if (!key || !key.trim()) {
|
|
478
|
+
console.log(` ${c.red}No key entered.${c.reset}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
472
481
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
saveCredentials({ api_key: key, agent_name:
|
|
482
|
+
process.stdout.write(` Validating...`);
|
|
483
|
+
const validation = await validateApiKey(key.trim());
|
|
484
|
+
if (validation.valid) {
|
|
485
|
+
const agentName = validation.agent_name || 'agent';
|
|
486
|
+
saveCredentials({ api_key: key.trim(), agent_name: agentName });
|
|
487
|
+
console.log(` ${c.green}valid!${c.reset}`);
|
|
478
488
|
console.log();
|
|
479
|
-
console.log(` ${icons.safe}
|
|
489
|
+
console.log(` ${icons.safe} Logged in as ${c.bold}${agentName}${c.reset}`);
|
|
490
|
+
console.log(` ${c.dim}Key saved to: ${USER_CRED_FILE}${c.reset}`);
|
|
480
491
|
} else {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
process.stdout.write(` Registering ${c.bold}${name}${c.reset}...`);
|
|
487
|
-
try {
|
|
488
|
-
const data = await registerAgent(name);
|
|
489
|
-
saveCredentials({ api_key: data.api_key, agent_name: data.agent_name });
|
|
490
|
-
console.log(` ${c.green}done!${c.reset}`);
|
|
491
|
-
console.log();
|
|
492
|
-
console.log(` ${icons.safe} Registered as ${c.bold}${data.agent_name}${c.reset}`);
|
|
493
|
-
console.log(` ${c.dim}Key: ${data.api_key.slice(0, 12)}...${c.reset}`);
|
|
494
|
-
console.log(` ${c.dim}Saved to: ${USER_CRED_FILE}${c.reset}`);
|
|
495
|
-
} catch (err) {
|
|
496
|
-
console.log(` ${c.red}failed${c.reset}`);
|
|
497
|
-
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
492
|
+
console.log(` ${c.red}invalid${c.reset}`);
|
|
493
|
+
console.log(` ${c.red}Key not recognized. Make sure you copied the full key from ${REGISTRY_URL}/profile${c.reset}`);
|
|
494
|
+
return;
|
|
500
495
|
}
|
|
501
496
|
|
|
502
497
|
console.log();
|
|
@@ -560,7 +555,7 @@ function banner() {
|
|
|
560
555
|
const ptsStr = `${fmtNum(cache.total_points)}pts`;
|
|
561
556
|
const auditsStr = `${fmtNum(cache.total_reports)} audits`;
|
|
562
557
|
const profile = [cache.agent_name, rankStr, ptsStr, auditsStr].filter(Boolean).join(' \u00b7 ');
|
|
563
|
-
console.log(` ${c.bold}${c.cyan}\
|
|
558
|
+
console.log(` ${c.bold}${c.cyan}\u25c6 AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset} ${c.dim}\u2502${c.reset} ${profile}`);
|
|
564
559
|
} else {
|
|
565
560
|
console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
|
|
566
561
|
console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
|
|
@@ -581,10 +576,14 @@ function elapsed(startMs) {
|
|
|
581
576
|
}
|
|
582
577
|
|
|
583
578
|
function riskBadge(score) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
579
|
+
const badge = score === 0 ? `${c.bgGreen}${c.bold}${c.white} SAFE ${c.reset}`
|
|
580
|
+
: score <= 10 ? `${c.bgGreen}${c.white} LOW ${c.reset}`
|
|
581
|
+
: score <= 30 ? `${c.bgYellow}${c.bold} CAUTION ${c.reset}`
|
|
582
|
+
: `${c.bgRed}${c.bold}${c.white} UNSAFE ${c.reset}`;
|
|
583
|
+
const filled = Math.min(Math.round(score / 20), 5);
|
|
584
|
+
const gaugeColor = score <= 10 ? c.green : score <= 30 ? c.yellow : c.red;
|
|
585
|
+
const gauge = `${gaugeColor}${'▰'.repeat(filled)}${c.dim}${'▱'.repeat(5 - filled)}${c.reset}`;
|
|
586
|
+
return `${badge} ${gauge} ${score}/100`;
|
|
588
587
|
}
|
|
589
588
|
|
|
590
589
|
function severityColor(sev) {
|
|
@@ -701,6 +700,47 @@ function sparkline(values) {
|
|
|
701
700
|
}).join('');
|
|
702
701
|
}
|
|
703
702
|
|
|
703
|
+
// ─── Section Header ─────────── labeled divider
|
|
704
|
+
function sectionHeader(title, width = 60) {
|
|
705
|
+
const dashAfter = Math.max(3, width - 5 - title.length);
|
|
706
|
+
return ` ${c.dim}───${c.reset} ${c.bold}${title}${c.reset} ${c.dim}${'─'.repeat(dashAfter)}${c.reset}`;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// █████░░░░░░░░░░░░░░ coverage bar
|
|
710
|
+
function coverageBar(filled, total, width = 20) {
|
|
711
|
+
if (total === 0) return `${c.dim}${'░'.repeat(width)}${c.reset} 0/0`;
|
|
712
|
+
const barFilled = Math.max(filled > 0 ? 1 : 0, Math.round((filled / total) * width));
|
|
713
|
+
const barEmpty = width - barFilled;
|
|
714
|
+
const pct = Math.round((filled / total) * 100);
|
|
715
|
+
const color = pct >= 80 ? c.green : pct >= 50 ? c.yellow : c.red;
|
|
716
|
+
return `${color}${'█'.repeat(barFilled)}${c.dim}${'░'.repeat(barEmpty)}${c.reset} ${filled}/${total} (${pct}%)`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Severity histogram for findings
|
|
720
|
+
function severityHistogram(findings) {
|
|
721
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
722
|
+
for (const f of findings) {
|
|
723
|
+
const sev = (f.severity || '').toLowerCase();
|
|
724
|
+
if (counts[sev] !== undefined) counts[sev]++;
|
|
725
|
+
}
|
|
726
|
+
const max = Math.max(...Object.values(counts), 1);
|
|
727
|
+
const lines = [];
|
|
728
|
+
for (const sev of ['critical', 'high', 'medium', 'low']) {
|
|
729
|
+
const count = counts[sev];
|
|
730
|
+
if (count === 0) continue;
|
|
731
|
+
const barLen = Math.max(1, Math.round((count / max) * 24));
|
|
732
|
+
const sc = severityColor(sev);
|
|
733
|
+
const label = sev.toUpperCase().padEnd(10);
|
|
734
|
+
lines.push(` ${sc}${label}${c.reset} ${sc}${'█'.repeat(barLen)}${c.reset}${' '.repeat(24 - barLen)} ${count}`);
|
|
735
|
+
}
|
|
736
|
+
return lines;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ▰▰▰▱ step progress indicator
|
|
740
|
+
function stepProgress(current, total) {
|
|
741
|
+
return `${c.cyan}${'▰'.repeat(current)}${c.dim}${'▱'.repeat(total - current)}${c.reset}`;
|
|
742
|
+
}
|
|
743
|
+
|
|
704
744
|
function fmtNum(n) {
|
|
705
745
|
if (n == null) return '0';
|
|
706
746
|
return n.toLocaleString('en-US');
|
|
@@ -715,7 +755,7 @@ function dashboardBanner() {
|
|
|
715
755
|
const ver = getVersion();
|
|
716
756
|
return [
|
|
717
757
|
` ${BOX.tl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.tr}`,
|
|
718
|
-
` ${BOX.v} ${c.bold}${c.cyan}
|
|
758
|
+
` ${BOX.v} ${c.bold}${c.cyan}◆ AgentAudit${c.reset} ${c.dim}v${ver}${c.reset}${' '.repeat(Math.max(0, 19 - ver.length))}${BOX.v}`,
|
|
719
759
|
` ${BOX.v} ${c.dim}Security Registry for AI Agents${c.reset} ${BOX.v}`,
|
|
720
760
|
` ${BOX.bl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.br}`,
|
|
721
761
|
];
|
|
@@ -1056,7 +1096,7 @@ function quickChecks(files) {
|
|
|
1056
1096
|
|
|
1057
1097
|
async function checkRegistry(slug) {
|
|
1058
1098
|
try {
|
|
1059
|
-
const res = await fetch(`${REGISTRY_URL}/api/
|
|
1099
|
+
const res = await fetch(`${REGISTRY_URL}/api/packages/${encodeURIComponent(slug)}`, {
|
|
1060
1100
|
signal: AbortSignal.timeout(5000),
|
|
1061
1101
|
});
|
|
1062
1102
|
if (res.ok) return await res.json();
|
|
@@ -1126,31 +1166,38 @@ function printScanResult(url, info, files, findings, registryData, duration) {
|
|
|
1126
1166
|
console.log(`${icons.pipe} ${c.dim}(no tools or prompts detected)${c.reset}`);
|
|
1127
1167
|
}
|
|
1128
1168
|
|
|
1129
|
-
// Findings
|
|
1169
|
+
// Findings with severity stripe
|
|
1130
1170
|
if (findings.length > 0) {
|
|
1131
|
-
console.log(
|
|
1132
|
-
console.log(
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const branch = isLast ? icons.treeLast : icons.tree;
|
|
1137
|
-
const pipeOrSpace = isLast ? ' ' : `${icons.pipe} `;
|
|
1171
|
+
console.log();
|
|
1172
|
+
console.log(sectionHeader(`Findings (${findings.length})`));
|
|
1173
|
+
console.log(` ${c.dim}static analysis — may include false positives${c.reset}`);
|
|
1174
|
+
console.log();
|
|
1175
|
+
for (const f of findings) {
|
|
1138
1176
|
const sc = severityColor(f.severity);
|
|
1139
|
-
console.log(
|
|
1140
|
-
console.log(
|
|
1177
|
+
console.log(` ${sc}┃${c.reset} ${sc}${f.severity.toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
|
|
1178
|
+
console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}:${f.line}${c.reset} ${c.dim}${f.snippet || ''}${c.reset}`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Severity histogram
|
|
1182
|
+
const histLines = severityHistogram(findings);
|
|
1183
|
+
if (histLines.length > 1) {
|
|
1184
|
+
console.log();
|
|
1185
|
+
console.log(sectionHeader('Severity'));
|
|
1186
|
+
for (const line of histLines) console.log(line);
|
|
1141
1187
|
}
|
|
1142
1188
|
}
|
|
1143
|
-
|
|
1189
|
+
|
|
1144
1190
|
// Registry status
|
|
1145
|
-
console.log(
|
|
1191
|
+
console.log();
|
|
1192
|
+
console.log(sectionHeader('Registry'));
|
|
1146
1193
|
if (registryData) {
|
|
1147
1194
|
const rd = registryData;
|
|
1148
1195
|
const riskScore = rd.risk_score ?? rd.latest_risk_score ?? 0;
|
|
1149
|
-
console.log(
|
|
1196
|
+
console.log(` ${riskBadge(riskScore)} ${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
|
|
1150
1197
|
} else {
|
|
1151
|
-
console.log(
|
|
1198
|
+
console.log(` ${c.dim}not audited yet${c.reset}`);
|
|
1152
1199
|
}
|
|
1153
|
-
|
|
1200
|
+
|
|
1154
1201
|
console.log();
|
|
1155
1202
|
}
|
|
1156
1203
|
|
|
@@ -1159,27 +1206,23 @@ function printSummary(results) {
|
|
|
1159
1206
|
const safe = results.filter(r => r.findings.length === 0).length;
|
|
1160
1207
|
const withFindings = total - safe;
|
|
1161
1208
|
const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
console.log(`
|
|
1209
|
+
const allFindings = results.flatMap(r => r.findings);
|
|
1210
|
+
|
|
1211
|
+
console.log(sectionHeader(`Summary — ${total} packages scanned`));
|
|
1165
1212
|
console.log();
|
|
1166
1213
|
if (safe > 0) console.log(` ${icons.safe} ${c.green}${safe} clean${c.reset}`);
|
|
1167
1214
|
if (withFindings > 0) console.log(` ${icons.caution} ${c.yellow}${withFindings} with findings${c.reset} (${totalFindings} total)`);
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (Object.keys(bySev).length > 0) {
|
|
1215
|
+
console.log();
|
|
1216
|
+
console.log(` ${coverageBar(safe, total)}`);
|
|
1217
|
+
|
|
1218
|
+
// Severity histogram
|
|
1219
|
+
const histLines = severityHistogram(allFindings);
|
|
1220
|
+
if (histLines.length > 0) {
|
|
1175
1221
|
console.log();
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
console.log(` ${severityIcon(sev)} ${bySev[sev]}× ${severityColor(sev)}${sev}${c.reset}`);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1222
|
+
console.log(sectionHeader('Severity'));
|
|
1223
|
+
for (const line of histLines) console.log(line);
|
|
1181
1224
|
}
|
|
1182
|
-
|
|
1225
|
+
|
|
1183
1226
|
console.log();
|
|
1184
1227
|
}
|
|
1185
1228
|
|
|
@@ -1236,48 +1279,290 @@ async function scanRepo(url) {
|
|
|
1236
1279
|
|
|
1237
1280
|
// ── Discover local MCP configs ──────────────────────────
|
|
1238
1281
|
|
|
1282
|
+
/**
|
|
1283
|
+
* Minimal YAML parser — extracts MCP server list entries from
|
|
1284
|
+
* Continue.dev (mcpServers: list) and Goose (extensions: list).
|
|
1285
|
+
* Zero dependencies. Only handles the subset of YAML used by these tools.
|
|
1286
|
+
*/
|
|
1287
|
+
function parseSimpleYaml(text, rootKey) {
|
|
1288
|
+
const result = { mcpServers: {} };
|
|
1289
|
+
const lines = text.split('\n');
|
|
1290
|
+
let inSection = false;
|
|
1291
|
+
let currentName = null;
|
|
1292
|
+
let currentServer = {};
|
|
1293
|
+
let collectingArgs = false;
|
|
1294
|
+
let argsIndent = -1;
|
|
1295
|
+
|
|
1296
|
+
for (const line of lines) {
|
|
1297
|
+
const trimmed = line.trimEnd();
|
|
1298
|
+
if (trimmed === '' || /^\s*#/.test(trimmed)) continue;
|
|
1299
|
+
const indent = line.search(/\S/);
|
|
1300
|
+
|
|
1301
|
+
if (indent === 0 && trimmed === rootKey + ':') { inSection = true; continue; }
|
|
1302
|
+
if (indent === 0 && inSection && /^\w/.test(trimmed)) {
|
|
1303
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
if (!inSection) continue;
|
|
1307
|
+
|
|
1308
|
+
const nameMatch = trimmed.match(/^\s*-\s+name:\s*(.+)/);
|
|
1309
|
+
if (nameMatch) {
|
|
1310
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1311
|
+
currentName = nameMatch[1].replace(/^["']|["']$/g, '');
|
|
1312
|
+
currentServer = {};
|
|
1313
|
+
collectingArgs = false;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (collectingArgs && indent > argsIndent) {
|
|
1318
|
+
const argVal = trimmed.match(/^\s*-\s+(.+)/);
|
|
1319
|
+
if (argVal) {
|
|
1320
|
+
if (!currentServer.args) currentServer.args = [];
|
|
1321
|
+
currentServer.args.push(argVal[1].replace(/^["']|["']$/g, ''));
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (collectingArgs && indent <= argsIndent) collectingArgs = false;
|
|
1326
|
+
if (!currentName) continue;
|
|
1327
|
+
|
|
1328
|
+
const kvMatch = trimmed.match(/^\s+(command|cmd|type|url):\s*(.+)/);
|
|
1329
|
+
if (kvMatch) {
|
|
1330
|
+
collectingArgs = false;
|
|
1331
|
+
const key = kvMatch[1] === 'cmd' ? 'command' : kvMatch[1];
|
|
1332
|
+
currentServer[key] = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const argsMatch = trimmed.match(/^\s+(args):\s*(.*)/);
|
|
1337
|
+
if (argsMatch) {
|
|
1338
|
+
const inlineArr = argsMatch[2].match(/^\[(.+)\]$/);
|
|
1339
|
+
if (inlineArr) {
|
|
1340
|
+
currentServer.args = inlineArr[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
1341
|
+
collectingArgs = false;
|
|
1342
|
+
} else {
|
|
1343
|
+
collectingArgs = true;
|
|
1344
|
+
argsIndent = indent;
|
|
1345
|
+
currentServer.args = [];
|
|
1346
|
+
}
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1351
|
+
return result;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Minimal TOML parser — extracts [mcp_servers.xxx] sections
|
|
1356
|
+
* from OpenAI Codex CLI config. Zero dependencies.
|
|
1357
|
+
*/
|
|
1358
|
+
function parseSimpleToml(text) {
|
|
1359
|
+
const result = { mcpServers: {} };
|
|
1360
|
+
const lines = text.split('\n');
|
|
1361
|
+
let currentName = null;
|
|
1362
|
+
let currentServer = {};
|
|
1363
|
+
|
|
1364
|
+
for (const line of lines) {
|
|
1365
|
+
const trimmed = line.trim();
|
|
1366
|
+
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
1367
|
+
|
|
1368
|
+
const sectionMatch = trimmed.match(/^\[mcp_servers\.(.+)\]$/);
|
|
1369
|
+
if (sectionMatch) {
|
|
1370
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1371
|
+
currentName = sectionMatch[1];
|
|
1372
|
+
currentServer = {};
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (trimmed.startsWith('[') && !trimmed.startsWith('[mcp_servers.')) {
|
|
1376
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1377
|
+
currentName = null;
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (!currentName) continue;
|
|
1381
|
+
|
|
1382
|
+
const strMatch = trimmed.match(/^(command|url)\s*=\s*"(.+?)"/);
|
|
1383
|
+
if (strMatch) { currentServer[strMatch[1]] = strMatch[2]; continue; }
|
|
1384
|
+
|
|
1385
|
+
const argsMatch = trimmed.match(/^args\s*=\s*\[(.+)\]/);
|
|
1386
|
+
if (argsMatch) {
|
|
1387
|
+
currentServer.args = argsMatch[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const boolMatch = trimmed.match(/^enabled\s*=\s*(true|false)/);
|
|
1392
|
+
if (boolMatch && boolMatch[1] === 'false') currentServer.disabled = true;
|
|
1393
|
+
}
|
|
1394
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1395
|
+
return result;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Comprehensive MCP config discovery across all major AI editors & tools.
|
|
1400
|
+
*
|
|
1401
|
+
* Supports: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code,
|
|
1402
|
+
* Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose,
|
|
1403
|
+
* OpenAI Codex CLI, Visual Studio — global + project-level configs.
|
|
1404
|
+
*/
|
|
1239
1405
|
function findMcpConfigs() {
|
|
1240
1406
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1241
1407
|
const platform = process.platform;
|
|
1242
|
-
|
|
1243
|
-
|
|
1408
|
+
const cwd = process.cwd();
|
|
1409
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
1410
|
+
|
|
1411
|
+
// Platform-specific app data directory
|
|
1412
|
+
// macOS: ~/Library/Application Support, Windows: ~/AppData/Roaming, Linux: ~/.config
|
|
1413
|
+
const appData = platform === 'darwin'
|
|
1414
|
+
? path.join(home, 'Library', 'Application Support')
|
|
1415
|
+
: platform === 'win32'
|
|
1416
|
+
? path.join(home, 'AppData', 'Roaming')
|
|
1417
|
+
: xdgConfig;
|
|
1418
|
+
|
|
1419
|
+
// Each candidate: { name, path, format: 'json'|'yaml'|'toml', key: top-level key }
|
|
1244
1420
|
const candidates = [
|
|
1245
|
-
// Claude Desktop
|
|
1246
|
-
{ name: 'Claude Desktop', path: path.join(home, '
|
|
1247
|
-
{ name: 'Claude Desktop', path: path.join(home, '
|
|
1248
|
-
{ name: 'Claude Desktop', path: path.join(
|
|
1249
|
-
|
|
1250
|
-
//
|
|
1251
|
-
{ name: '
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
//
|
|
1255
|
-
{ name: '
|
|
1256
|
-
|
|
1257
|
-
|
|
1421
|
+
// ── Claude Desktop ──
|
|
1422
|
+
...(platform === 'darwin' ? [{ name: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1423
|
+
...(platform === 'win32' ? [{ name: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1424
|
+
...(platform === 'linux' ? [{ name: 'Claude Desktop', path: path.join(xdgConfig, 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1425
|
+
|
|
1426
|
+
// ── Claude Code ──
|
|
1427
|
+
{ name: 'Claude Code', path: path.join(home, '.claude.json'), format: 'json', key: 'mcpServers' },
|
|
1428
|
+
{ name: 'Claude Code', path: path.join(home, '.claude', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1429
|
+
|
|
1430
|
+
// ── Cursor (global) ──
|
|
1431
|
+
{ name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1432
|
+
|
|
1433
|
+
// ── Windsurf / Codeium ──
|
|
1434
|
+
{ name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), format: 'json', key: 'mcpServers' },
|
|
1435
|
+
|
|
1436
|
+
// ── VS Code (global mcp.json — uses 'servers' key) ──
|
|
1437
|
+
...(platform === 'darwin' ? [{ name: 'VS Code', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
|
|
1438
|
+
...(platform === 'win32' ? [{ name: 'VS Code', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
|
|
1439
|
+
...(platform === 'linux' ? [{ name: 'VS Code', path: path.join(xdgConfig, 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
|
|
1440
|
+
|
|
1441
|
+
// ── VS Code settings.json (mcp.servers nested key) ──
|
|
1442
|
+
...(platform === 'darwin' ? [{ name: 'VS Code (settings)', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
|
|
1443
|
+
...(platform === 'win32' ? [{ name: 'VS Code (settings)', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
|
|
1444
|
+
...(platform === 'linux' ? [{ name: 'VS Code (settings)', path: path.join(xdgConfig, 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
|
|
1445
|
+
|
|
1446
|
+
// ── Cline (VS Code extension) ──
|
|
1447
|
+
...(platform === 'darwin' ? [{ name: 'Cline', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1448
|
+
...(platform === 'win32' ? [{ name: 'Cline', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1449
|
+
...(platform === 'linux' ? [{ name: 'Cline', path: path.join(xdgConfig, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1450
|
+
|
|
1451
|
+
// ── Roo Code (VS Code extension) ──
|
|
1452
|
+
...(platform === 'darwin' ? [{ name: 'Roo Code', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1453
|
+
...(platform === 'win32' ? [{ name: 'Roo Code', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1454
|
+
...(platform === 'linux' ? [{ name: 'Roo Code', path: path.join(xdgConfig, 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1455
|
+
|
|
1456
|
+
// ── Amazon Q Developer ──
|
|
1457
|
+
{ name: 'Amazon Q', path: path.join(home, '.aws', 'amazonq', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1458
|
+
{ name: 'Amazon Q (IDE)', path: path.join(home, '.aws', 'amazonq', 'default.json'), format: 'json', key: 'mcpServers' },
|
|
1459
|
+
|
|
1460
|
+
// ── Gemini CLI ──
|
|
1461
|
+
{ name: 'Gemini CLI', path: path.join(home, '.gemini', 'settings.json'), format: 'json', key: 'mcpServers' },
|
|
1462
|
+
|
|
1463
|
+
// ── Zed (macOS + Linux only, uses 'context_servers' key) ──
|
|
1464
|
+
...(platform === 'darwin' ? [{ name: 'Zed', path: path.join(home, '.zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
|
|
1465
|
+
...(platform === 'linux' ? [{ name: 'Zed', path: path.join(xdgConfig, 'zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
|
|
1466
|
+
|
|
1467
|
+
// ── Continue.dev ──
|
|
1468
|
+
{ name: 'Continue', path: path.join(home, '.continue', 'config.json'), format: 'json', key: 'mcpServers' },
|
|
1469
|
+
{ name: 'Continue', path: path.join(home, '.continue', 'config.yaml'), format: 'yaml', key: 'mcpServers' },
|
|
1470
|
+
|
|
1471
|
+
// ── Goose (Block/Square) ──
|
|
1472
|
+
{ name: 'Goose', path: path.join(xdgConfig, 'goose', 'config.yaml'), format: 'yaml', key: 'extensions' },
|
|
1473
|
+
|
|
1474
|
+
// ── OpenAI Codex CLI ──
|
|
1475
|
+
{ name: 'Codex CLI', path: path.join(home, '.codex', 'config.toml'), format: 'toml', key: 'mcp_servers' },
|
|
1476
|
+
|
|
1477
|
+
// ── Visual Studio (Windows only) ──
|
|
1478
|
+
...(platform === 'win32' ? [{ name: 'Visual Studio', path: path.join(home, '.mcp.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1479
|
+
|
|
1480
|
+
// ── Project-level configs (cwd) ──
|
|
1481
|
+
{ name: 'Claude Code (project)', path: path.join(cwd, '.mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1482
|
+
{ name: 'Cursor (project)', path: path.join(cwd, '.cursor', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1483
|
+
{ name: 'VS Code (project)', path: path.join(cwd, '.vscode', 'mcp.json'), format: 'json', key: 'servers' },
|
|
1484
|
+
{ name: 'Roo Code (project)', path: path.join(cwd, '.roo', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1485
|
+
{ name: 'Amazon Q (project)', path: path.join(cwd, '.amazonq', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1486
|
+
{ name: 'Gemini CLI (project)', path: path.join(cwd, '.gemini', 'settings.json'), format: 'json', key: 'mcpServers' },
|
|
1487
|
+
...(platform !== 'win32' ? [{ name: 'Zed (project)', path: path.join(cwd, '.zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
|
|
1488
|
+
{ name: 'Codex CLI (project)', path: path.join(cwd, '.codex', 'config.toml'), format: 'toml', key: 'mcp_servers' },
|
|
1258
1489
|
];
|
|
1259
|
-
|
|
1260
|
-
//
|
|
1490
|
+
|
|
1491
|
+
// Test config override
|
|
1261
1492
|
if (process.env.AGENTAUDIT_TEST_CONFIG) {
|
|
1262
|
-
candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG });
|
|
1493
|
+
candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG, format: 'json', key: 'mcpServers' });
|
|
1263
1494
|
}
|
|
1264
|
-
|
|
1265
|
-
//
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1495
|
+
|
|
1496
|
+
// Continue.dev mcpServers drop-in directory (individual JSON files)
|
|
1497
|
+
const continueDropIn = path.join(home, '.continue', 'mcpServers');
|
|
1498
|
+
try {
|
|
1499
|
+
if (fs.existsSync(continueDropIn)) {
|
|
1500
|
+
for (const f of fs.readdirSync(continueDropIn)) {
|
|
1501
|
+
if (f.endsWith('.json')) {
|
|
1502
|
+
candidates.push({ name: 'Continue (drop-in)', path: path.join(continueDropIn, f), format: 'json', key: 'mcpServers' });
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
} catch {}
|
|
1507
|
+
|
|
1508
|
+
// Project-level Continue.dev drop-ins
|
|
1509
|
+
const cwdContinueDropIn = path.join(cwd, '.continue', 'mcpServers');
|
|
1510
|
+
try {
|
|
1511
|
+
if (fs.existsSync(cwdContinueDropIn)) {
|
|
1512
|
+
for (const f of fs.readdirSync(cwdContinueDropIn)) {
|
|
1513
|
+
if (f.endsWith('.json')) {
|
|
1514
|
+
candidates.push({ name: 'Continue (project drop-in)', path: path.join(cwdContinueDropIn, f), format: 'json', key: 'mcpServers' });
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
} catch {}
|
|
1519
|
+
|
|
1272
1520
|
const found = [];
|
|
1521
|
+
const seenPaths = new Set();
|
|
1522
|
+
|
|
1273
1523
|
for (const c of candidates) {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1524
|
+
const resolved = path.resolve(c.path);
|
|
1525
|
+
if (seenPaths.has(resolved)) continue;
|
|
1526
|
+
if (!fs.existsSync(c.path)) continue;
|
|
1527
|
+
seenPaths.add(resolved);
|
|
1528
|
+
|
|
1529
|
+
try {
|
|
1530
|
+
const raw = fs.readFileSync(c.path, 'utf8');
|
|
1531
|
+
let content;
|
|
1532
|
+
|
|
1533
|
+
if (c.format === 'yaml') {
|
|
1534
|
+
content = parseSimpleYaml(raw, c.key);
|
|
1535
|
+
} else if (c.format === 'toml') {
|
|
1536
|
+
content = parseSimpleToml(raw);
|
|
1537
|
+
} else {
|
|
1538
|
+
content = JSON.parse(raw);
|
|
1539
|
+
// Normalize different JSON key structures to mcpServers
|
|
1540
|
+
if (c.key === 'mcp.servers' && content.mcp?.servers) {
|
|
1541
|
+
content = { mcpServers: content.mcp.servers };
|
|
1542
|
+
} else if (c.key === 'context_servers' && content.context_servers) {
|
|
1543
|
+
// Zed: normalize nested { command: { path, args } } → { command, args }
|
|
1544
|
+
const normalized = {};
|
|
1545
|
+
for (const [name, cfg] of Object.entries(content.context_servers)) {
|
|
1546
|
+
if (cfg.command && typeof cfg.command === 'object') {
|
|
1547
|
+
normalized[name] = { command: cfg.command.path || cfg.command.command, args: cfg.command.args || [], env: cfg.command.env || {} };
|
|
1548
|
+
} else {
|
|
1549
|
+
normalized[name] = cfg;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
content = { mcpServers: normalized };
|
|
1553
|
+
} else if (c.key === 'servers' && content.servers && !content.mcpServers) {
|
|
1554
|
+
content = { mcpServers: content.servers };
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Only include configs that actually have servers
|
|
1559
|
+
const servers = content?.mcpServers || content?.servers || {};
|
|
1560
|
+
if (Object.keys(servers).length > 0) {
|
|
1561
|
+
found.push({ name: c.name, path: c.path, content });
|
|
1562
|
+
}
|
|
1563
|
+
} catch {}
|
|
1280
1564
|
}
|
|
1565
|
+
|
|
1281
1566
|
return found;
|
|
1282
1567
|
}
|
|
1283
1568
|
|
|
@@ -1456,7 +1741,7 @@ async function discoverCommand(options = {}) {
|
|
|
1456
1741
|
const interactiveAudit = options.audit || false;
|
|
1457
1742
|
|
|
1458
1743
|
if (!jsonMode) {
|
|
1459
|
-
console.log(` ${c.bold}Discovering MCP servers
|
|
1744
|
+
console.log(` ${c.bold}Discovering MCP servers across all AI tools...${c.reset}`);
|
|
1460
1745
|
console.log();
|
|
1461
1746
|
}
|
|
1462
1747
|
|
|
@@ -1464,13 +1749,16 @@ async function discoverCommand(options = {}) {
|
|
|
1464
1749
|
|
|
1465
1750
|
if (configs.length === 0) {
|
|
1466
1751
|
console.log(` ${c.yellow}No MCP configurations found.${c.reset}`);
|
|
1467
|
-
console.log(` ${c.dim}Searched: Claude Desktop, Cursor, Windsurf, VS Code
|
|
1752
|
+
console.log(` ${c.dim}Searched 15+ tools: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code,${c.reset}`);
|
|
1753
|
+
console.log(` ${c.dim}Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose, Codex CLI${c.reset}`);
|
|
1468
1754
|
console.log();
|
|
1469
|
-
console.log(` ${c.dim}MCP config locations:${c.reset}`);
|
|
1470
|
-
console.log(` ${c.dim} Claude:
|
|
1471
|
-
console.log(` ${c.dim}
|
|
1472
|
-
console.log(` ${c.dim}
|
|
1473
|
-
console.log(` ${c.dim}
|
|
1755
|
+
console.log(` ${c.dim}Common MCP config locations:${c.reset}`);
|
|
1756
|
+
console.log(` ${c.dim} Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json${c.reset}`);
|
|
1757
|
+
console.log(` ${c.dim} Claude Code: ~/.claude.json${c.reset}`);
|
|
1758
|
+
console.log(` ${c.dim} Cursor: ~/.cursor/mcp.json${c.reset}`);
|
|
1759
|
+
console.log(` ${c.dim} Windsurf: ~/.codeium/windsurf/mcp_config.json${c.reset}`);
|
|
1760
|
+
console.log(` ${c.dim} VS Code: (platform)/Code/User/mcp.json${c.reset}`);
|
|
1761
|
+
console.log(` ${c.dim} Project-level: .mcp.json / .cursor/mcp.json / .vscode/mcp.json${c.reset}`);
|
|
1474
1762
|
console.log();
|
|
1475
1763
|
return;
|
|
1476
1764
|
}
|
|
@@ -1533,7 +1821,7 @@ async function discoverCommand(options = {}) {
|
|
|
1533
1821
|
const riskScore = regData.risk_score ?? regData.latest_risk_score ?? 0;
|
|
1534
1822
|
const hasOfficial = regData.has_official_audit;
|
|
1535
1823
|
console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
|
|
1536
|
-
console.log(`${pipe} ${riskBadge(riskScore)}
|
|
1824
|
+
console.log(`${pipe} ${riskBadge(riskScore)} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
|
|
1537
1825
|
if (resolvedUrl) allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: true, regData });
|
|
1538
1826
|
} else {
|
|
1539
1827
|
unauditedServers++;
|
|
@@ -1556,11 +1844,14 @@ async function discoverCommand(options = {}) {
|
|
|
1556
1844
|
}
|
|
1557
1845
|
|
|
1558
1846
|
// Summary
|
|
1559
|
-
console.log(
|
|
1560
|
-
console.log(` ${c.bold}Summary${c.reset} ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`);
|
|
1847
|
+
console.log(sectionHeader(`Summary — ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`));
|
|
1561
1848
|
console.log();
|
|
1562
1849
|
if (auditedServers > 0) console.log(` ${icons.safe} ${c.green}${auditedServers} audited${c.reset}`);
|
|
1563
1850
|
if (unauditedServers > 0) console.log(` ${icons.caution} ${c.yellow}${unauditedServers} not audited${c.reset}`);
|
|
1851
|
+
if (totalServers > 0) {
|
|
1852
|
+
console.log();
|
|
1853
|
+
console.log(` ${coverageBar(auditedServers, totalServers)}`);
|
|
1854
|
+
}
|
|
1564
1855
|
console.log();
|
|
1565
1856
|
|
|
1566
1857
|
// --scan: automatically scan all servers with resolved source URLs (git-cloneable only)
|
|
@@ -1576,8 +1867,8 @@ async function discoverCommand(options = {}) {
|
|
|
1576
1867
|
});
|
|
1577
1868
|
const skipped = allServersWithUrls.filter(s => s.sourceUrl && !isCloneable(s.sourceUrl));
|
|
1578
1869
|
if (dedupedTargets.length > 0) {
|
|
1579
|
-
console.log(
|
|
1580
|
-
console.log(` ${c.bold}${icons.scan}
|
|
1870
|
+
console.log(sectionHeader(`Auto-scanning ${dedupedTargets.length} server${dedupedTargets.length !== 1 ? 's' : ''}`));
|
|
1871
|
+
console.log(` ${c.bold}${icons.scan} Starting scans...${c.reset}`);
|
|
1581
1872
|
if (skipped.length > 0) {
|
|
1582
1873
|
console.log(` ${c.dim}(${skipped.length} skipped — no cloneable source URL)${c.reset}`);
|
|
1583
1874
|
}
|
|
@@ -1591,8 +1882,7 @@ async function discoverCommand(options = {}) {
|
|
|
1591
1882
|
|
|
1592
1883
|
if (scanResults.length > 1) {
|
|
1593
1884
|
// Print combined scan summary
|
|
1594
|
-
console.log(
|
|
1595
|
-
console.log(` ${c.bold}Scan Summary${c.reset} ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`);
|
|
1885
|
+
console.log(sectionHeader(`Scan Summary — ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`));
|
|
1596
1886
|
console.log();
|
|
1597
1887
|
|
|
1598
1888
|
let totalFindings = 0;
|
|
@@ -1701,7 +1991,7 @@ async function auditRepo(url) {
|
|
|
1701
1991
|
console.log();
|
|
1702
1992
|
|
|
1703
1993
|
// Step 1: Clone
|
|
1704
|
-
process.stdout.write(` ${
|
|
1994
|
+
process.stdout.write(` ${stepProgress(1, 4)} Cloning repository...`);
|
|
1705
1995
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
1706
1996
|
const repoPath = path.join(tmpDir, 'repo');
|
|
1707
1997
|
try {
|
|
@@ -1716,12 +2006,12 @@ async function auditRepo(url) {
|
|
|
1716
2006
|
}
|
|
1717
2007
|
|
|
1718
2008
|
// Step 2: Collect files
|
|
1719
|
-
process.stdout.write(` ${
|
|
2009
|
+
process.stdout.write(` ${stepProgress(2, 4)} Collecting source files...`);
|
|
1720
2010
|
const files = collectFiles(repoPath);
|
|
1721
2011
|
console.log(` ${c.green}${files.length} files${c.reset}`);
|
|
1722
2012
|
|
|
1723
2013
|
// Step 3: Build audit payload
|
|
1724
|
-
process.stdout.write(` ${
|
|
2014
|
+
process.stdout.write(` ${stepProgress(3, 4)} Preparing audit payload...`);
|
|
1725
2015
|
const auditPrompt = loadAuditPrompt();
|
|
1726
2016
|
|
|
1727
2017
|
let codeBlock = '';
|
|
@@ -1794,7 +2084,7 @@ async function auditRepo(url) {
|
|
|
1794
2084
|
|
|
1795
2085
|
// We have an API key — run LLM audit
|
|
1796
2086
|
const modelLabel = modelOverride ? `${activeProvider} → ${activeLlm.model}` : activeProvider;
|
|
1797
|
-
process.stdout.write(` ${
|
|
2087
|
+
process.stdout.write(` ${stepProgress(4, 4)} Running LLM analysis ${c.dim}(${modelLabel})${c.reset}...`);
|
|
1798
2088
|
|
|
1799
2089
|
const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
|
|
1800
2090
|
const userMessage = [
|
|
@@ -2008,17 +2298,26 @@ async function auditRepo(url) {
|
|
|
2008
2298
|
// Display results
|
|
2009
2299
|
console.log();
|
|
2010
2300
|
const riskScore = report.risk_score || 0;
|
|
2011
|
-
console.log(
|
|
2301
|
+
console.log(sectionHeader('Result'));
|
|
2302
|
+
console.log(` ${riskBadge(riskScore)}`);
|
|
2012
2303
|
console.log();
|
|
2013
|
-
|
|
2304
|
+
|
|
2014
2305
|
if (report.findings && report.findings.length > 0) {
|
|
2015
|
-
console.log(`
|
|
2306
|
+
console.log(sectionHeader(`Findings (${report.findings.length})`));
|
|
2016
2307
|
console.log();
|
|
2017
2308
|
for (const f of report.findings) {
|
|
2018
2309
|
const sc = severityColor(f.severity);
|
|
2019
|
-
console.log(` ${
|
|
2020
|
-
if (f.file) console.log(`
|
|
2021
|
-
if (f.description) console.log(`
|
|
2310
|
+
console.log(` ${sc}┃${c.reset} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
|
|
2311
|
+
if (f.file) console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
|
|
2312
|
+
if (f.description) console.log(` ${sc}┃${c.reset} ${c.dim}${f.description.slice(0, 120)}${c.reset}`);
|
|
2313
|
+
console.log();
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// Severity histogram
|
|
2317
|
+
const histLines = severityHistogram(report.findings);
|
|
2318
|
+
if (histLines.length > 1) {
|
|
2319
|
+
console.log(sectionHeader('Severity'));
|
|
2320
|
+
for (const line of histLines) console.log(line);
|
|
2022
2321
|
console.log();
|
|
2023
2322
|
}
|
|
2024
2323
|
} else {
|
|
@@ -2046,7 +2345,7 @@ async function auditRepo(url) {
|
|
|
2046
2345
|
if (res.ok) {
|
|
2047
2346
|
const data = await res.json();
|
|
2048
2347
|
console.log(` ${c.green}done${c.reset}`);
|
|
2049
|
-
console.log(` ${c.dim}Report: ${REGISTRY_URL}/
|
|
2348
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/packages/${slug}${c.reset}`);
|
|
2050
2349
|
} else {
|
|
2051
2350
|
let errBody = '';
|
|
2052
2351
|
try { errBody = await res.text(); } catch {}
|
|
@@ -2059,27 +2358,22 @@ async function auditRepo(url) {
|
|
|
2059
2358
|
console.log(` ${c.yellow}failed${c.reset}`);
|
|
2060
2359
|
}
|
|
2061
2360
|
} else if (process.stdin.isTTY) {
|
|
2062
|
-
// No credentials —
|
|
2063
|
-
console.log();
|
|
2064
|
-
console.log(` ${c.bold}Share this audit with the community?${c.reset}`);
|
|
2065
|
-
console.log(` ${c.dim}Uploading helps others assess package security. Account is free.${c.reset}`);
|
|
2361
|
+
// No credentials — prompt to paste key or set up
|
|
2066
2362
|
console.log();
|
|
2067
|
-
console.log(` ${c.bold}
|
|
2068
|
-
console.log(` ${c.
|
|
2363
|
+
console.log(` ${c.bold}Want to upload this report to agentaudit.dev?${c.reset}`);
|
|
2364
|
+
console.log(` ${c.dim}Create an API key at ${c.cyan}${REGISTRY_URL}/profile${c.dim} (sign in with GitHub)${c.reset}`);
|
|
2069
2365
|
console.log();
|
|
2070
|
-
const
|
|
2071
|
-
if (
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2366
|
+
const pastedKey = await askQuestion(` Paste API key ${c.dim}(or Enter to skip)${c.reset}: `);
|
|
2367
|
+
if (pastedKey && pastedKey.trim()) {
|
|
2368
|
+
process.stdout.write(` Validating...`);
|
|
2369
|
+
const validation = await validateApiKey(pastedKey.trim());
|
|
2370
|
+
if (validation.valid) {
|
|
2371
|
+
const agentName = validation.agent_name || 'agent';
|
|
2372
|
+
saveCredentials({ api_key: pastedKey.trim(), agent_name: agentName });
|
|
2373
|
+
creds = { api_key: pastedKey.trim(), agent_name: agentName };
|
|
2374
|
+
console.log(` ${c.green}valid!${c.reset}`);
|
|
2375
|
+
process.stdout.write(` Uploading report...`);
|
|
2076
2376
|
try {
|
|
2077
|
-
process.stdout.write(` Registering ${c.bold}${name}${c.reset}...`);
|
|
2078
|
-
const regData = await registerAgent(name);
|
|
2079
|
-
saveCredentials({ api_key: regData.api_key, agent_name: regData.agent_name });
|
|
2080
|
-
console.log(` ${c.green}done!${c.reset}`);
|
|
2081
|
-
creds = { api_key: regData.api_key, agent_name: regData.agent_name };
|
|
2082
|
-
process.stdout.write(` Uploading report...`);
|
|
2083
2377
|
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
2084
2378
|
method: 'POST',
|
|
2085
2379
|
headers: {
|
|
@@ -2091,7 +2385,7 @@ async function auditRepo(url) {
|
|
|
2091
2385
|
});
|
|
2092
2386
|
if (res.ok) {
|
|
2093
2387
|
console.log(` ${c.green}done${c.reset}`);
|
|
2094
|
-
console.log(` ${c.dim}Report: ${REGISTRY_URL}/
|
|
2388
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/packages/${slug}${c.reset}`);
|
|
2095
2389
|
} else {
|
|
2096
2390
|
console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
|
|
2097
2391
|
}
|
|
@@ -2099,10 +2393,13 @@ async function auditRepo(url) {
|
|
|
2099
2393
|
console.log(` ${c.red}failed${c.reset}`);
|
|
2100
2394
|
console.log(` ${c.dim}${err.message}${c.reset}`);
|
|
2101
2395
|
}
|
|
2396
|
+
} else {
|
|
2397
|
+
console.log(` ${c.red}invalid key${c.reset}`);
|
|
2398
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to configure.${c.reset}`);
|
|
2102
2399
|
}
|
|
2103
2400
|
}
|
|
2104
2401
|
} else {
|
|
2105
|
-
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to
|
|
2402
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to configure your API key and upload reports${c.reset}`);
|
|
2106
2403
|
}
|
|
2107
2404
|
|
|
2108
2405
|
console.log();
|
|
@@ -2131,7 +2428,7 @@ async function checkPackage(name) {
|
|
|
2131
2428
|
console.log(` ${c.bold}${name}${c.reset} ${riskBadge(riskScore)}`);
|
|
2132
2429
|
console.log(` ${c.dim}Risk Score: ${riskScore}/100${c.reset}`);
|
|
2133
2430
|
if (data.source_url) console.log(` ${c.dim}Source: ${data.source_url}${c.reset}`);
|
|
2134
|
-
console.log(` ${c.dim}Registry: ${REGISTRY_URL}/
|
|
2431
|
+
console.log(` ${c.dim}Registry: ${REGISTRY_URL}/packages/${name}${c.reset}`);
|
|
2135
2432
|
if (data.has_official_audit) console.log(` ${c.green}✔ Officially audited${c.reset}`);
|
|
2136
2433
|
console.log();
|
|
2137
2434
|
}
|
|
@@ -2255,7 +2552,7 @@ function renderLeaderboardTab(data, width, opts = {}) {
|
|
|
2255
2552
|
}
|
|
2256
2553
|
|
|
2257
2554
|
const maxPts = leaderboard[0]?.total_points || 1;
|
|
2258
|
-
const medals = [
|
|
2555
|
+
const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
|
|
2259
2556
|
|
|
2260
2557
|
for (let i = 0; i < leaderboard.length; i++) {
|
|
2261
2558
|
const entry = leaderboard[i];
|
|
@@ -2659,7 +2956,7 @@ async function leaderboardCommand(args) {
|
|
|
2659
2956
|
}
|
|
2660
2957
|
|
|
2661
2958
|
const maxPts = data[0]?.total_points || 1;
|
|
2662
|
-
const medals = [
|
|
2959
|
+
const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
|
|
2663
2960
|
const barW = 24;
|
|
2664
2961
|
|
|
2665
2962
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -3005,7 +3302,9 @@ async function main() {
|
|
|
3005
3302
|
discover: [
|
|
3006
3303
|
`${c.bold}agentaudit discover${c.reset} [options]`,
|
|
3007
3304
|
``,
|
|
3008
|
-
`Find MCP servers
|
|
3305
|
+
`Find MCP servers across 15+ AI tools (Claude Desktop, Claude Code, Cursor, VS Code,`,
|
|
3306
|
+
`Windsurf, Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose, Codex CLI).`,
|
|
3307
|
+
`Checks global + project-level configs on macOS, Windows, and Linux.`,
|
|
3009
3308
|
``,
|
|
3010
3309
|
`${c.bold}Options:${c.reset}`,
|
|
3011
3310
|
` --quick, -s Auto-scan all discovered servers (regex-based)`,
|
|
@@ -3193,6 +3492,21 @@ async function main() {
|
|
|
3193
3492
|
` agentaudit find fastmcp`,
|
|
3194
3493
|
],
|
|
3195
3494
|
find: null, // alias → search
|
|
3495
|
+
profile: [
|
|
3496
|
+
`${c.bold}agentaudit profile${c.reset}`,
|
|
3497
|
+
``,
|
|
3498
|
+
`Show your AgentAudit profile — rank, points, audit stats,`,
|
|
3499
|
+
`and a link to your public profile on agentaudit.dev.`,
|
|
3500
|
+
``,
|
|
3501
|
+
`Requires being logged in (run ${c.cyan}agentaudit setup${c.reset} first).`,
|
|
3502
|
+
``,
|
|
3503
|
+
`${c.bold}Options:${c.reset}`,
|
|
3504
|
+
` --json Machine-readable JSON output`,
|
|
3505
|
+
``,
|
|
3506
|
+
`${c.bold}Examples:${c.reset}`,
|
|
3507
|
+
` agentaudit profile`,
|
|
3508
|
+
` agentaudit profile --json`,
|
|
3509
|
+
],
|
|
3196
3510
|
};
|
|
3197
3511
|
|
|
3198
3512
|
// Show subcommand help: `agentaudit help <cmd>` or `agentaudit <cmd> --help`
|
|
@@ -3255,6 +3569,7 @@ async function main() {
|
|
|
3255
3569
|
console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
|
|
3256
3570
|
console.log(` ${c.cyan}setup${c.reset} Log in to agentaudit.dev (for report uploads)`);
|
|
3257
3571
|
console.log(` ${c.cyan}status${c.reset} Show current config + auth status`);
|
|
3572
|
+
console.log(` ${c.cyan}profile${c.reset} Your profile — rank, points, audit stats`);
|
|
3258
3573
|
console.log();
|
|
3259
3574
|
console.log(` ${c.bold}FLAGS${c.reset}`);
|
|
3260
3575
|
console.log(` ${c.dim}--json Machine-readable JSON output${c.reset}`);
|
|
@@ -3413,6 +3728,103 @@ async function main() {
|
|
|
3413
3728
|
return;
|
|
3414
3729
|
}
|
|
3415
3730
|
|
|
3731
|
+
if (command === 'profile') {
|
|
3732
|
+
const creds = loadCredentials();
|
|
3733
|
+
if (!creds) {
|
|
3734
|
+
console.log(` ${c.yellow}Not logged in.${c.reset}`);
|
|
3735
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to link your API key.${c.reset}`);
|
|
3736
|
+
console.log();
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
const agentName = creds.agent_name || 'unknown';
|
|
3741
|
+
console.log(` ${c.bold}Profile${c.reset} ${c.cyan}${agentName}${c.reset}`);
|
|
3742
|
+
console.log();
|
|
3743
|
+
|
|
3744
|
+
try {
|
|
3745
|
+
process.stdout.write(` ${c.dim}Fetching profile data...${c.reset}`);
|
|
3746
|
+
const [agentRes, lbRes] = await Promise.all([
|
|
3747
|
+
fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(agentName)}`, {
|
|
3748
|
+
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
3749
|
+
signal: AbortSignal.timeout(10_000),
|
|
3750
|
+
}).then(r => r.ok ? r.json() : null),
|
|
3751
|
+
fetch(`${REGISTRY_URL}/api/leaderboard?limit=100`, {
|
|
3752
|
+
signal: AbortSignal.timeout(10_000),
|
|
3753
|
+
}).then(r => r.ok ? r.json() : null),
|
|
3754
|
+
]);
|
|
3755
|
+
process.stdout.write('\r\x1b[K');
|
|
3756
|
+
|
|
3757
|
+
if (!agentRes) {
|
|
3758
|
+
console.log(` ${c.yellow}Could not fetch profile data.${c.reset}`);
|
|
3759
|
+
console.log(` ${c.dim}Your account may not have submitted any audits yet.${c.reset}`);
|
|
3760
|
+
console.log();
|
|
3761
|
+
console.log(` ${c.dim}Web profile: ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
|
|
3762
|
+
console.log();
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
let rank = null;
|
|
3767
|
+
if (Array.isArray(lbRes)) {
|
|
3768
|
+
const idx = lbRes.findIndex(e => e.agent_name === agentName);
|
|
3769
|
+
if (idx >= 0) rank = idx + 1;
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
// Update cache
|
|
3773
|
+
saveProfileCache({
|
|
3774
|
+
agent_name: agentName,
|
|
3775
|
+
rank,
|
|
3776
|
+
total_points: agentRes.total_points || 0,
|
|
3777
|
+
total_reports: agentRes.total_reports || 0,
|
|
3778
|
+
});
|
|
3779
|
+
|
|
3780
|
+
if (jsonMode) {
|
|
3781
|
+
console.log(JSON.stringify({
|
|
3782
|
+
agent_name: agentName,
|
|
3783
|
+
rank: rank ? { position: rank, total: lbRes?.length || 0 } : null,
|
|
3784
|
+
total_points: agentRes.total_points || 0,
|
|
3785
|
+
total_reports: agentRes.total_reports || 0,
|
|
3786
|
+
total_findings_submitted: agentRes.total_findings_submitted || 0,
|
|
3787
|
+
total_findings_confirmed: agentRes.total_findings_confirmed || 0,
|
|
3788
|
+
profile_url: `${REGISTRY_URL}/profile`,
|
|
3789
|
+
}, null, 2));
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
const boxW = 44;
|
|
3794
|
+
const rankStr = rank ? `#${rank} of ${lbRes.length}` : '—';
|
|
3795
|
+
const pts = agentRes.total_points || 0;
|
|
3796
|
+
const audits = agentRes.total_reports || 0;
|
|
3797
|
+
const findingsSubmitted = agentRes.total_findings_submitted || 0;
|
|
3798
|
+
const findingsConfirmed = agentRes.total_findings_confirmed || 0;
|
|
3799
|
+
const ptsBar = renderBar(pts, Math.max(pts, 1000), boxW - 16);
|
|
3800
|
+
|
|
3801
|
+
const contentLines = [
|
|
3802
|
+
'',
|
|
3803
|
+
`${c.bold}${c.cyan}${agentName}${c.reset}${' '.repeat(Math.max(1, boxW - 6 - agentName.length - rankStr.length))}${c.bold}${rankStr}${c.reset}`,
|
|
3804
|
+
'',
|
|
3805
|
+
`Points ${c.bold}${padLeft(fmtNum(pts), 8)}${c.reset}`,
|
|
3806
|
+
`Audits ${c.bold}${padLeft(fmtNum(audits), 8)}${c.reset}`,
|
|
3807
|
+
`Findings ${c.bold}${padLeft(fmtNum(findingsSubmitted), 8)}${c.reset} ${c.dim}(${fmtNum(findingsConfirmed)} confirmed)${c.reset}`,
|
|
3808
|
+
'',
|
|
3809
|
+
ptsBar,
|
|
3810
|
+
'',
|
|
3811
|
+
`${c.dim}${REGISTRY_URL}/profile${c.reset}`,
|
|
3812
|
+
`${c.dim}Key: ${creds.api_key.slice(0, 12)}...${c.reset}`,
|
|
3813
|
+
'',
|
|
3814
|
+
];
|
|
3815
|
+
const boxLines = drawBox('Profile', contentLines, boxW + 4);
|
|
3816
|
+
for (const line of boxLines) console.log(line);
|
|
3817
|
+
console.log();
|
|
3818
|
+
} catch (err) {
|
|
3819
|
+
process.stdout.write('\r\x1b[K');
|
|
3820
|
+
console.log(` ${c.yellow}Failed to fetch profile: ${err.message}${c.reset}`);
|
|
3821
|
+
console.log();
|
|
3822
|
+
console.log(` ${c.dim}Web profile: ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
|
|
3823
|
+
console.log();
|
|
3824
|
+
}
|
|
3825
|
+
return;
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3416
3828
|
if (command === 'model') {
|
|
3417
3829
|
const newModel = targets.filter(t => !t.startsWith('--'))[0];
|
|
3418
3830
|
const config = loadLlmConfig();
|