agentaudit 3.9.48 → 3.10.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 +591 -1110
- package/index.mjs +615 -687
- package/package.json +47 -45
- package/postinstall.mjs +18 -0
package/index.mjs
CHANGED
|
@@ -1,687 +1,615 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* AgentAudit MCP Server
|
|
4
|
-
*
|
|
5
|
-
* Security audit capabilities via Model Context Protocol.
|
|
6
|
-
*
|
|
7
|
-
* Tools:
|
|
8
|
-
* - discover_servers Find locally installed MCP servers + check registry status
|
|
9
|
-
* - audit_package Clone a repo, return source code + audit prompt for LLM analysis
|
|
10
|
-
* - submit_report Upload a completed audit report to agentaudit.dev
|
|
11
|
-
* - check_package Look up a package in the AgentAudit registry
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* npx agentaudit (starts MCP server via stdio)
|
|
15
|
-
* node index.mjs (same)
|
|
16
|
-
*
|
|
17
|
-
* Configure in Claude/Cursor/Windsurf:
|
|
18
|
-
* { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
22
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
|
-
import {
|
|
24
|
-
CallToolRequestSchema,
|
|
25
|
-
ListToolsRequestSchema,
|
|
26
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
27
|
-
import fs from 'fs';
|
|
28
|
-
import os from 'os';
|
|
29
|
-
import path from 'path';
|
|
30
|
-
import { execSync } from 'child_process';
|
|
31
|
-
import { fileURLToPath } from 'url';
|
|
32
|
-
|
|
33
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
-
const SKILL_DIR = path.resolve(__dirname);
|
|
35
|
-
const REGISTRY_URL = 'https://agentaudit.dev';
|
|
36
|
-
const MAX_FILE_SIZE = 50_000;
|
|
37
|
-
const MAX_TOTAL_SIZE = 300_000;
|
|
38
|
-
const SKIP_DIRS = new Set([
|
|
39
|
-
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
40
|
-
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
41
|
-
'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
|
|
42
|
-
'examples', 'example', 'fixtures', '.github', '.vscode', '.idea',
|
|
43
|
-
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
44
|
-
]);
|
|
45
|
-
const SKIP_EXTENSIONS = new Set([
|
|
46
|
-
'.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff',
|
|
47
|
-
'.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.tar', '.gz',
|
|
48
|
-
'.map', '.min.js', '.min.css', '.d.ts', '.pyc', '.pyo', '.so',
|
|
49
|
-
'.dylib', '.dll', '.exe', '.bin', '.dat', '.db', '.sqlite',
|
|
50
|
-
]);
|
|
51
|
-
const PRIORITY_FILES = [
|
|
52
|
-
'index.js', 'index.ts', 'index.mjs', 'main.js', 'main.ts', 'main.py',
|
|
53
|
-
'app.js', 'app.ts', 'app.py', 'server.js', 'server.ts', 'server.py',
|
|
54
|
-
'cli.js', 'cli.ts', 'cli.py', '__init__.py', '__main__.py',
|
|
55
|
-
'package.json', 'pyproject.toml', 'setup.py', 'setup.cfg',
|
|
56
|
-
'Cargo.toml', 'go.mod', 'SKILL.md', 'skill.md',
|
|
57
|
-
'Makefile', 'Dockerfile', 'docker-compose.yml',
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
// ── Credentials ─────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
function loadApiKey() {
|
|
63
|
-
if (process.env.AGENTAUDIT_API_KEY) return process.env.AGENTAUDIT_API_KEY;
|
|
64
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
65
|
-
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
66
|
-
const paths = [
|
|
67
|
-
path.join(SKILL_DIR, 'config', 'credentials.json'),
|
|
68
|
-
path.join(xdg, 'agentaudit', 'credentials.json'),
|
|
69
|
-
];
|
|
70
|
-
for (const p of paths) {
|
|
71
|
-
if (fs.existsSync(p)) {
|
|
72
|
-
try {
|
|
73
|
-
const key = JSON.parse(fs.readFileSync(p, 'utf8')).api_key;
|
|
74
|
-
if (key) return key;
|
|
75
|
-
} catch {}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return '';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function loadAuditPrompt() {
|
|
82
|
-
const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
|
|
83
|
-
if (fs.existsSync(promptPath)) return fs.readFileSync(promptPath, 'utf8');
|
|
84
|
-
return 'ERROR: audit-prompt.md not found at ' + promptPath;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── File Collection ─────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
90
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
91
|
-
let entries;
|
|
92
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
93
|
-
catch { return collected; }
|
|
94
|
-
entries.sort((a, b) => {
|
|
95
|
-
const aP = PRIORITY_FILES.includes(a.name) ? 0 : 1;
|
|
96
|
-
const bP = PRIORITY_FILES.includes(b.name) ? 0 : 1;
|
|
97
|
-
return aP - bP || a.name.localeCompare(b.name);
|
|
98
|
-
});
|
|
99
|
-
for (const entry of entries) {
|
|
100
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
|
|
101
|
-
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
102
|
-
const fullPath = path.join(dir, entry.name);
|
|
103
|
-
if (entry.isDirectory()) {
|
|
104
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
105
|
-
collectFiles(fullPath, relPath, collected, totalSize);
|
|
106
|
-
} else {
|
|
107
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
108
|
-
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
109
|
-
try {
|
|
110
|
-
const stat = fs.statSync(fullPath);
|
|
111
|
-
if (stat.size > MAX_FILE_SIZE) {
|
|
112
|
-
collected.push({ path: relPath, content: `[FILE TOO LARGE: ${stat.size} bytes — skipped]` });
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (stat.size === 0) continue;
|
|
116
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
117
|
-
totalSize.bytes += content.length;
|
|
118
|
-
collected.push({ path: relPath, content });
|
|
119
|
-
} catch {}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return collected;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Repo Helpers ────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
if (srv.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
``,
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
``,
|
|
445
|
-
`##
|
|
446
|
-
``,
|
|
447
|
-
`
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
`
|
|
453
|
-
|
|
454
|
-
`
|
|
455
|
-
` "
|
|
456
|
-
` "
|
|
457
|
-
` "
|
|
458
|
-
`
|
|
459
|
-
`
|
|
460
|
-
`
|
|
461
|
-
`
|
|
462
|
-
`
|
|
463
|
-
`
|
|
464
|
-
` "
|
|
465
|
-
` "
|
|
466
|
-
` "
|
|
467
|
-
` "
|
|
468
|
-
` "
|
|
469
|
-
`
|
|
470
|
-
`
|
|
471
|
-
`
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
if (!
|
|
500
|
-
return { content: [{ type: 'text', text: 'Error:
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
summary += `Reports: ${totalReports} from ${uniqueAgents} independent auditor${uniqueAgents !== 1 ? 's' : ''}\n`;
|
|
617
|
-
summary += `Confidence: ${confidence}\n`;
|
|
618
|
-
if (confidence === 'unverified') summary += `⚠️ Only 1 audit exists — consider running your own for independent verification.\n`;
|
|
619
|
-
if (confidence === 'low') summary += `⚠️ Limited independent verification — more auditors needed for consensus.\n`;
|
|
620
|
-
|
|
621
|
-
// Details section
|
|
622
|
-
summary += `\n--- Details ---\n`;
|
|
623
|
-
summary += `Package: ${package_name}\n`;
|
|
624
|
-
summary += `Status: ${official}\n`;
|
|
625
|
-
summary += `Last Audited: ${auditedAt}\n`;
|
|
626
|
-
if (version) summary += `Audited Version: ${version}\n`;
|
|
627
|
-
if (data.source_url) summary += `Source: ${data.source_url}\n`;
|
|
628
|
-
summary += `Registry: ${REGISTRY_URL}/skills/${package_name}\n`;
|
|
629
|
-
|
|
630
|
-
return { content: [{ type: 'text', text: summary }] };
|
|
631
|
-
} catch (err) {
|
|
632
|
-
return { content: [{ type: 'text', text: `Registry lookup failed: ${err.message}` }] };
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
case 'agent_stats': {
|
|
637
|
-
const creds = loadCredentials();
|
|
638
|
-
if (!creds?.api_key || !creds?.agent_name) {
|
|
639
|
-
return { content: [{ type: 'text', text: '⚠️ Not logged in. Run `agentaudit setup` first to register.' }] };
|
|
640
|
-
}
|
|
641
|
-
try {
|
|
642
|
-
// Fetch agent profile
|
|
643
|
-
const profileRes = await fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(creds.agent_name)}`, { signal: AbortSignal.timeout(8000) });
|
|
644
|
-
// Fetch leaderboard for rank
|
|
645
|
-
const lbRes = await fetch(`${REGISTRY_URL}/api/leaderboard`, { signal: AbortSignal.timeout(8000) });
|
|
646
|
-
|
|
647
|
-
let stats = '';
|
|
648
|
-
stats += `🛡️ AgentAudit — ${creds.agent_name}\n\n`;
|
|
649
|
-
|
|
650
|
-
if (profileRes.ok) {
|
|
651
|
-
const profile = await profileRes.json();
|
|
652
|
-
stats += `📊 Your Stats:\n`;
|
|
653
|
-
stats += ` Reports: ${profile.total_reports || 0}\n`;
|
|
654
|
-
stats += ` Findings: ${profile.total_findings_submitted || 0}\n`;
|
|
655
|
-
stats += ` Points: ${profile.total_points || 0}\n`;
|
|
656
|
-
if (profile.critical_found > 0) stats += ` Critical: ${profile.critical_found} 🔴\n`;
|
|
657
|
-
if (profile.high_found > 0) stats += ` High: ${profile.high_found} 🟠\n`;
|
|
658
|
-
stats += ` Packages: ${profile.skills_audited?.length || 0} unique\n`;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (lbRes.ok) {
|
|
662
|
-
const agents = await lbRes.json();
|
|
663
|
-
const idx = Array.isArray(agents) ? agents.findIndex(a => (a.agent_name || '').toLowerCase() === creds.agent_name.toLowerCase()) : -1;
|
|
664
|
-
if (idx >= 0) {
|
|
665
|
-
const rank = idx + 1;
|
|
666
|
-
const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : `#${rank}`;
|
|
667
|
-
stats += `\n🏆 Leaderboard: ${medal} of ${agents.length}\n`;
|
|
668
|
-
if (agents[idx].is_official) stats += ` ✔ Official Auditor\n`;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
stats += `\nProfile: ${REGISTRY_URL}/agents/${encodeURIComponent(creds.agent_name)}`;
|
|
673
|
-
return { content: [{ type: 'text', text: stats }] };
|
|
674
|
-
} catch (err) {
|
|
675
|
-
return { content: [{ type: 'text', text: `Failed to fetch stats: ${err.message}` }] };
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
default:
|
|
680
|
-
return { content: [{ type: 'text', text: `Unknown tool: ${name}. Available: discover_servers, audit_package, submit_report, check_package, agent_stats` }] };
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
// ── Start ────────────────────────────────────────────────
|
|
685
|
-
|
|
686
|
-
const transport = new StdioServerTransport();
|
|
687
|
-
await server.connect(transport);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentAudit MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Security audit capabilities via Model Context Protocol.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - discover_servers Find locally installed MCP servers + check registry status
|
|
9
|
+
* - audit_package Clone a repo, return source code + audit prompt for LLM analysis
|
|
10
|
+
* - submit_report Upload a completed audit report to agentaudit.dev
|
|
11
|
+
* - check_package Look up a package in the AgentAudit registry
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* npx agentaudit (starts MCP server via stdio)
|
|
15
|
+
* node index.mjs (same)
|
|
16
|
+
*
|
|
17
|
+
* Configure in Claude/Cursor/Windsurf:
|
|
18
|
+
* { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
22
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
|
+
import {
|
|
24
|
+
CallToolRequestSchema,
|
|
25
|
+
ListToolsRequestSchema,
|
|
26
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import os from 'os';
|
|
29
|
+
import path from 'path';
|
|
30
|
+
import { execSync, execFileSync } from 'child_process';
|
|
31
|
+
import { fileURLToPath } from 'url';
|
|
32
|
+
|
|
33
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const SKILL_DIR = path.resolve(__dirname);
|
|
35
|
+
const REGISTRY_URL = 'https://agentaudit.dev';
|
|
36
|
+
const MAX_FILE_SIZE = 50_000;
|
|
37
|
+
const MAX_TOTAL_SIZE = 300_000;
|
|
38
|
+
const SKIP_DIRS = new Set([
|
|
39
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
40
|
+
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
41
|
+
'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
|
|
42
|
+
'examples', 'example', 'fixtures', '.github', '.vscode', '.idea',
|
|
43
|
+
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
44
|
+
]);
|
|
45
|
+
const SKIP_EXTENSIONS = new Set([
|
|
46
|
+
'.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff',
|
|
47
|
+
'.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.tar', '.gz',
|
|
48
|
+
'.map', '.min.js', '.min.css', '.d.ts', '.pyc', '.pyo', '.so',
|
|
49
|
+
'.dylib', '.dll', '.exe', '.bin', '.dat', '.db', '.sqlite',
|
|
50
|
+
]);
|
|
51
|
+
const PRIORITY_FILES = [
|
|
52
|
+
'index.js', 'index.ts', 'index.mjs', 'main.js', 'main.ts', 'main.py',
|
|
53
|
+
'app.js', 'app.ts', 'app.py', 'server.js', 'server.ts', 'server.py',
|
|
54
|
+
'cli.js', 'cli.ts', 'cli.py', '__init__.py', '__main__.py',
|
|
55
|
+
'package.json', 'pyproject.toml', 'setup.py', 'setup.cfg',
|
|
56
|
+
'Cargo.toml', 'go.mod', 'SKILL.md', 'skill.md',
|
|
57
|
+
'Makefile', 'Dockerfile', 'docker-compose.yml',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// ── Credentials ─────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function loadApiKey() {
|
|
63
|
+
if (process.env.AGENTAUDIT_API_KEY) return process.env.AGENTAUDIT_API_KEY;
|
|
64
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
65
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
66
|
+
const paths = [
|
|
67
|
+
path.join(SKILL_DIR, 'config', 'credentials.json'),
|
|
68
|
+
path.join(xdg, 'agentaudit', 'credentials.json'),
|
|
69
|
+
];
|
|
70
|
+
for (const p of paths) {
|
|
71
|
+
if (fs.existsSync(p)) {
|
|
72
|
+
try {
|
|
73
|
+
const key = JSON.parse(fs.readFileSync(p, 'utf8')).api_key;
|
|
74
|
+
if (key) return key;
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadAuditPrompt() {
|
|
82
|
+
const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
|
|
83
|
+
if (fs.existsSync(promptPath)) return fs.readFileSync(promptPath, 'utf8');
|
|
84
|
+
return 'ERROR: audit-prompt.md not found at ' + promptPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── File Collection ─────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
90
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
91
|
+
let entries;
|
|
92
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
93
|
+
catch { return collected; }
|
|
94
|
+
entries.sort((a, b) => {
|
|
95
|
+
const aP = PRIORITY_FILES.includes(a.name) ? 0 : 1;
|
|
96
|
+
const bP = PRIORITY_FILES.includes(b.name) ? 0 : 1;
|
|
97
|
+
return aP - bP || a.name.localeCompare(b.name);
|
|
98
|
+
});
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
|
|
101
|
+
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
102
|
+
const fullPath = path.join(dir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
105
|
+
collectFiles(fullPath, relPath, collected, totalSize);
|
|
106
|
+
} else {
|
|
107
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
108
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
109
|
+
try {
|
|
110
|
+
const stat = fs.statSync(fullPath);
|
|
111
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
112
|
+
collected.push({ path: relPath, content: `[FILE TOO LARGE: ${stat.size} bytes — skipped]` });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (stat.size === 0) continue;
|
|
116
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
117
|
+
totalSize.bytes += content.length;
|
|
118
|
+
collected.push({ path: relPath, content });
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return collected;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Repo Helpers ────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function validateGitUrl(url) {
|
|
128
|
+
if (/[;&|`$(){}!\n\r]/.test(url)) {
|
|
129
|
+
throw new Error(`Rejected URL with suspicious characters: ${url.slice(0, 80)}`);
|
|
130
|
+
}
|
|
131
|
+
if (!/^(https?:\/\/|git@|git:\/\/|ssh:\/\/)/.test(url) && !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(url)) {
|
|
132
|
+
throw new Error(`Invalid repository URL: ${url.slice(0, 80)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cloneRepo(sourceUrl) {
|
|
137
|
+
validateGitUrl(sourceUrl);
|
|
138
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
139
|
+
try {
|
|
140
|
+
execFileSync('git', ['clone', '--depth', '1', sourceUrl, path.join(tmpDir, 'repo')], {
|
|
141
|
+
timeout: 30_000, stdio: 'pipe',
|
|
142
|
+
});
|
|
143
|
+
return path.join(tmpDir, 'repo');
|
|
144
|
+
} catch (err) {
|
|
145
|
+
throw new Error(`Failed to clone ${sourceUrl}: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function cleanupRepo(repoPath) {
|
|
150
|
+
try { fs.rmSync(path.dirname(repoPath), { recursive: true, force: true }); } catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function slugFromUrl(url) {
|
|
154
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/.\s]+)/);
|
|
155
|
+
if (match) return match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
156
|
+
return url.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Discover local MCP configs ──────────────────────────
|
|
160
|
+
|
|
161
|
+
function discoverMcpServers() {
|
|
162
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
163
|
+
const candidates = [
|
|
164
|
+
{ platform: 'Claude Desktop', path: path.join(home, '.claude', 'mcp.json') },
|
|
165
|
+
{ platform: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
|
|
166
|
+
{ platform: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
|
|
167
|
+
{ platform: 'Claude Desktop', path: path.join(home, '.config', 'claude', 'claude_desktop_config.json') },
|
|
168
|
+
{ platform: 'Cursor', path: path.join(home, '.cursor', 'mcp.json') },
|
|
169
|
+
{ platform: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
|
|
170
|
+
{ platform: 'VS Code', path: path.join(home, '.vscode', 'mcp.json') },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const results = [];
|
|
174
|
+
|
|
175
|
+
for (const c of candidates) {
|
|
176
|
+
if (!fs.existsSync(c.path)) {
|
|
177
|
+
results.push({ platform: c.platform, config_path: c.path, status: 'not found', servers: [] });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let content;
|
|
181
|
+
try { content = JSON.parse(fs.readFileSync(c.path, 'utf8')); }
|
|
182
|
+
catch { results.push({ platform: c.platform, config_path: c.path, status: 'parse error', servers: [] }); continue; }
|
|
183
|
+
|
|
184
|
+
const serverMap = content.mcpServers || content.servers || {};
|
|
185
|
+
const servers = [];
|
|
186
|
+
for (const [name, cfg] of Object.entries(serverMap)) {
|
|
187
|
+
const allArgs = [cfg.command, ...(cfg.args || [])].filter(Boolean).join(' ');
|
|
188
|
+
const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
|
|
189
|
+
const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
|
|
190
|
+
let remoteService = null;
|
|
191
|
+
if (cfg.url) {
|
|
192
|
+
try {
|
|
193
|
+
const hostParts = new URL(cfg.url).hostname.split('.');
|
|
194
|
+
remoteService = hostParts.length === 3 ? hostParts[1] : hostParts[0];
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
servers.push({
|
|
198
|
+
name,
|
|
199
|
+
command: cfg.command || null,
|
|
200
|
+
args: cfg.args || [],
|
|
201
|
+
url: cfg.url || null,
|
|
202
|
+
npm_package: npxMatch?.[1] || null,
|
|
203
|
+
pip_package: pyMatch?.[1] || null,
|
|
204
|
+
remote_service: remoteService,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
results.push({ platform: c.platform, config_path: c.path, status: 'found', server_count: servers.length, servers });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return results;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function resolveSourceUrl(server) {
|
|
214
|
+
if (server.npm_package) {
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npm_package)}`, {
|
|
217
|
+
signal: AbortSignal.timeout(5000),
|
|
218
|
+
});
|
|
219
|
+
if (res.ok) {
|
|
220
|
+
const data = await res.json();
|
|
221
|
+
let repoUrl = data.repository?.url;
|
|
222
|
+
if (repoUrl) {
|
|
223
|
+
repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
|
|
224
|
+
if (repoUrl.startsWith('http')) return repoUrl;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
return `https://www.npmjs.com/package/${server.npm_package}`;
|
|
229
|
+
}
|
|
230
|
+
if (server.pip_package) {
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pip_package)}/json`, {
|
|
233
|
+
signal: AbortSignal.timeout(5000),
|
|
234
|
+
});
|
|
235
|
+
if (res.ok) {
|
|
236
|
+
const data = await res.json();
|
|
237
|
+
const urls = data.info?.project_urls || {};
|
|
238
|
+
const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
|
|
239
|
+
if (source && source.startsWith('http')) return source;
|
|
240
|
+
}
|
|
241
|
+
} catch {}
|
|
242
|
+
return `https://pypi.org/project/${server.pip_package}/`;
|
|
243
|
+
}
|
|
244
|
+
// URL-based remote MCP — try npm with common naming patterns
|
|
245
|
+
if (server.remote_service) {
|
|
246
|
+
for (const tryName of [
|
|
247
|
+
`@${server.remote_service}/mcp-server-${server.remote_service}`,
|
|
248
|
+
`${server.remote_service}-mcp`,
|
|
249
|
+
`mcp-server-${server.remote_service}`,
|
|
250
|
+
]) {
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
|
|
253
|
+
signal: AbortSignal.timeout(3000),
|
|
254
|
+
});
|
|
255
|
+
if (res.ok) {
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
let repoUrl = data.repository?.url;
|
|
258
|
+
if (repoUrl) {
|
|
259
|
+
repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
260
|
+
if (repoUrl.startsWith('http')) return repoUrl;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function checkRegistry(slug) {
|
|
270
|
+
try {
|
|
271
|
+
const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
|
|
272
|
+
signal: AbortSignal.timeout(5000),
|
|
273
|
+
});
|
|
274
|
+
if (res.ok) return await res.json();
|
|
275
|
+
} catch {}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── MCP Server ───────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
const server = new Server(
|
|
282
|
+
{ name: 'agentaudit', version: '3.9.8' },
|
|
283
|
+
{ capabilities: { tools: {} } }
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
287
|
+
tools: [
|
|
288
|
+
{
|
|
289
|
+
name: 'discover_servers',
|
|
290
|
+
description: 'Scan local config files to list ALREADY INSTALLED MCP servers (Claude Desktop, Cursor, Windsurf, VS Code). Use ONLY when the user wants to review/list their existing servers. Do NOT use this when the user wants to install, evaluate, or look up a specific package — use check_package for that instead.',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
check_registry: {
|
|
295
|
+
type: 'boolean',
|
|
296
|
+
description: 'If true, also check each discovered server against the AgentAudit registry (default: true)',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'audit_package',
|
|
303
|
+
description: 'Deep security audit of a Git repository. Clones the repo and returns source code with a 3-pass audit methodology (UNDERSTAND → DETECT → CLASSIFY). You then analyze the code and call submit_report with findings. Use check_package FIRST to see if an audit already exists — only use this for unaudited packages or when a fresh audit is requested.',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
source_url: {
|
|
308
|
+
type: 'string',
|
|
309
|
+
description: 'Git repository URL to audit (e.g., https://github.com/owner/repo)',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
required: ['source_url'],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'submit_report',
|
|
317
|
+
description: 'Submit a completed security audit report to the AgentAudit registry (agentaudit.dev). Call this after you have analyzed the code from audit_package. The report becomes publicly available and helps other agents make install decisions.',
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
properties: {
|
|
321
|
+
report: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
description: 'The audit report JSON object. Required fields: skill_slug, source_url, risk_score (0-100), result (safe|caution|unsafe), findings (array), findings_count, max_severity, package_type.',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ['report'],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'check_package',
|
|
331
|
+
description: 'Look up a package in the AgentAudit security registry. USE THIS FIRST whenever the user wants to install, add, evaluate, or learn about a specific MCP server or package. Returns risk score, findings, and official audit status if available. If the package is not yet in the registry, suggests running an audit. This is the go-to tool for any "is this safe?" or "should I install this?" question.',
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: 'object',
|
|
334
|
+
properties: {
|
|
335
|
+
package_name: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
description: 'Package name or slug to look up (e.g., "fastmcp", "mongodb-mcp-server")',
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
required: ['package_name'],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
347
|
+
const { name, arguments: args } = request.params;
|
|
348
|
+
|
|
349
|
+
switch (name) {
|
|
350
|
+
|
|
351
|
+
// ── discover_servers ──────────────────────────────────
|
|
352
|
+
case 'discover_servers': {
|
|
353
|
+
const doRegistryCheck = args.check_registry !== false;
|
|
354
|
+
const configs = discoverMcpServers();
|
|
355
|
+
const foundConfigs = configs.filter(c => c.status === 'found');
|
|
356
|
+
const allServers = foundConfigs.flatMap(c => c.servers.map(s => ({ ...s, platform: c.platform })));
|
|
357
|
+
|
|
358
|
+
let text = `# Discovered MCP Servers\n\n`;
|
|
359
|
+
text += `Scanned ${configs.length} config locations. Found ${foundConfigs.length} config(s) with ${allServers.length} server(s).\n\n`;
|
|
360
|
+
|
|
361
|
+
for (const config of configs) {
|
|
362
|
+
if (config.status === 'not found') continue;
|
|
363
|
+
text += `## ${config.platform}\n`;
|
|
364
|
+
text += `Config: \`${config.config_path}\`\n\n`;
|
|
365
|
+
|
|
366
|
+
if (config.servers.length === 0) {
|
|
367
|
+
text += `No servers configured.\n\n`;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const srv of config.servers) {
|
|
372
|
+
const slug = srv.npm_package?.replace(/^@/, '').replace(/\//g, '-')
|
|
373
|
+
|| srv.pip_package?.replace(/[^a-z0-9-]/gi, '-')
|
|
374
|
+
|| srv.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
375
|
+
|
|
376
|
+
text += `### ${srv.name}\n`;
|
|
377
|
+
if (srv.url) {
|
|
378
|
+
text += `- URL: \`${srv.url}\`\n`;
|
|
379
|
+
} else {
|
|
380
|
+
text += `- Command: \`${[srv.command, ...srv.args].filter(Boolean).join(' ')}\`\n`;
|
|
381
|
+
}
|
|
382
|
+
if (srv.npm_package) text += `- npm: ${srv.npm_package}\n`;
|
|
383
|
+
if (srv.pip_package) text += `- pip: ${srv.pip_package}\n`;
|
|
384
|
+
if (srv.remote_service) text += `- Service: ${srv.remote_service}\n`;
|
|
385
|
+
|
|
386
|
+
if (doRegistryCheck) {
|
|
387
|
+
const regData = await checkRegistry(slug);
|
|
388
|
+
if (regData) {
|
|
389
|
+
const risk = regData.risk_score ?? regData.latest_risk_score ?? 0;
|
|
390
|
+
const official = regData.has_official_audit ? ' (official)' : '';
|
|
391
|
+
text += `- **Registry: ✅ Audited** — Risk ${risk}/100${official}\n`;
|
|
392
|
+
text += `- Report: ${REGISTRY_URL}/skills/${slug}\n`;
|
|
393
|
+
} else {
|
|
394
|
+
const sourceUrl = await resolveSourceUrl(srv);
|
|
395
|
+
text += `- **Registry: ⚠️ Not audited** — no audit report found\n`;
|
|
396
|
+
if (sourceUrl) {
|
|
397
|
+
text += `- Source: ${sourceUrl}\n`;
|
|
398
|
+
text += `- To audit: call \`audit_package\` with source_url \`${sourceUrl}\`\n`;
|
|
399
|
+
} else {
|
|
400
|
+
text += `- Source URL unknown — check the package's GitHub/npm page\n`;
|
|
401
|
+
text += `- To audit: find the source URL, then call \`audit_package\`\n`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
text += `\n`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (allServers.length === 0) {
|
|
410
|
+
text += `No MCP servers found. Config locations searched:\n`;
|
|
411
|
+
text += `- Claude Desktop: ~/.claude/mcp.json\n`;
|
|
412
|
+
text += `- Cursor: ~/.cursor/mcp.json\n`;
|
|
413
|
+
text += `- Windsurf: ~/.codeium/windsurf/mcp_config.json\n`;
|
|
414
|
+
text += `- VS Code: ~/.vscode/mcp.json\n`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { content: [{ type: 'text', text }] };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── audit_package ─────────────────────────────────────
|
|
421
|
+
case 'audit_package': {
|
|
422
|
+
const { source_url } = args;
|
|
423
|
+
if (!source_url || !source_url.startsWith('http')) {
|
|
424
|
+
return { content: [{ type: 'text', text: 'Error: source_url must be a valid HTTP(S) URL' }] };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let repoPath;
|
|
428
|
+
try {
|
|
429
|
+
repoPath = cloneRepo(source_url);
|
|
430
|
+
const files = collectFiles(repoPath);
|
|
431
|
+
const slug = slugFromUrl(source_url);
|
|
432
|
+
const auditPrompt = loadAuditPrompt();
|
|
433
|
+
|
|
434
|
+
let codeBlock = '';
|
|
435
|
+
for (const file of files) {
|
|
436
|
+
codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const response = [
|
|
440
|
+
`# Security Audit: ${slug}`,
|
|
441
|
+
``,
|
|
442
|
+
`**Source:** ${source_url}`,
|
|
443
|
+
`**Files collected:** ${files.length}`,
|
|
444
|
+
``,
|
|
445
|
+
`## Your Task`,
|
|
446
|
+
``,
|
|
447
|
+
`1. Analyze the source code below using the 3-pass audit methodology`,
|
|
448
|
+
`2. Call \`submit_report\` with your findings as JSON`,
|
|
449
|
+
``,
|
|
450
|
+
`## Report Format`,
|
|
451
|
+
``,
|
|
452
|
+
`Your report JSON must include:`,
|
|
453
|
+
'```json',
|
|
454
|
+
`{`,
|
|
455
|
+
` "skill_slug": "${slug}",`,
|
|
456
|
+
` "source_url": "${source_url}",`,
|
|
457
|
+
` "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
|
|
458
|
+
` "risk_score": <0-100>,`,
|
|
459
|
+
` "result": "<safe|caution|unsafe>",`,
|
|
460
|
+
` "max_severity": "<none|low|medium|high|critical>",`,
|
|
461
|
+
` "findings_count": <number>,`,
|
|
462
|
+
` "findings": [`,
|
|
463
|
+
` {`,
|
|
464
|
+
` "id": "FINDING_ID",`,
|
|
465
|
+
` "title": "Short title",`,
|
|
466
|
+
` "severity": "<low|medium|high|critical>",`,
|
|
467
|
+
` "category": "<category>",`,
|
|
468
|
+
` "description": "Detailed description",`,
|
|
469
|
+
` "file": "path/to/file.js",`,
|
|
470
|
+
` "line": <line_number>,`,
|
|
471
|
+
` "remediation": "How to fix",`,
|
|
472
|
+
` "confidence": "<low|medium|high>",`,
|
|
473
|
+
` "is_by_design": <true|false>`,
|
|
474
|
+
` }`,
|
|
475
|
+
` ]`,
|
|
476
|
+
`}`,
|
|
477
|
+
'```',
|
|
478
|
+
``,
|
|
479
|
+
`## Audit Methodology`,
|
|
480
|
+
``,
|
|
481
|
+
auditPrompt,
|
|
482
|
+
``,
|
|
483
|
+
`## Source Code`,
|
|
484
|
+
``,
|
|
485
|
+
codeBlock,
|
|
486
|
+
].join('\n');
|
|
487
|
+
|
|
488
|
+
return { content: [{ type: 'text', text: response }] };
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
491
|
+
} finally {
|
|
492
|
+
if (repoPath) cleanupRepo(repoPath);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── submit_report ─────────────────────────────────────
|
|
497
|
+
case 'submit_report': {
|
|
498
|
+
const { report } = args;
|
|
499
|
+
if (!report || typeof report !== 'object') {
|
|
500
|
+
return { content: [{ type: 'text', text: 'Error: report must be a JSON object' }] };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const apiKey = loadApiKey();
|
|
504
|
+
if (!apiKey) {
|
|
505
|
+
return { content: [{ type: 'text', text: 'Error: No API key configured. Run `npx agentaudit setup` or set AGENTAUDIT_API_KEY.' }] };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const required = ['skill_slug', 'source_url', 'risk_score', 'result'];
|
|
509
|
+
for (const field of required) {
|
|
510
|
+
if (report[field] == null) {
|
|
511
|
+
return { content: [{ type: 'text', text: `Error: Missing required field "${field}" in report` }] };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!Array.isArray(report.findings)) report.findings = [];
|
|
516
|
+
report.findings_count = report.findings.length;
|
|
517
|
+
if (!report.max_severity) {
|
|
518
|
+
const severities = ['critical', 'high', 'medium', 'low', 'none'];
|
|
519
|
+
report.max_severity = report.findings.reduce((max, f) => {
|
|
520
|
+
const fi = severities.indexOf(f.severity);
|
|
521
|
+
const mi = severities.indexOf(max);
|
|
522
|
+
return fi < mi ? f.severity : max;
|
|
523
|
+
}, 'none');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
528
|
+
method: 'POST',
|
|
529
|
+
headers: {
|
|
530
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
531
|
+
'Content-Type': 'application/json',
|
|
532
|
+
},
|
|
533
|
+
body: JSON.stringify(report),
|
|
534
|
+
signal: AbortSignal.timeout(60_000),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const body = await res.text();
|
|
538
|
+
let data;
|
|
539
|
+
try { data = JSON.parse(body); } catch { data = { raw: body }; }
|
|
540
|
+
|
|
541
|
+
if (res.ok) {
|
|
542
|
+
return { content: [{ type: 'text', text: `✅ Report submitted!\n\nReport ID: ${data.report_id || 'unknown'}\nURL: ${REGISTRY_URL}/skills/${report.skill_slug}\nRisk: ${report.risk_score}/100 (${report.result})\nFindings: ${report.findings_count}` }] };
|
|
543
|
+
} else {
|
|
544
|
+
return { content: [{ type: 'text', text: `Upload failed (HTTP ${res.status}): ${JSON.stringify(data, null, 2)}` }] };
|
|
545
|
+
}
|
|
546
|
+
} catch (err) {
|
|
547
|
+
return { content: [{ type: 'text', text: `Upload error: ${err.message}` }] };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── check_package ─────────────────────────────────────
|
|
552
|
+
case 'check_package': {
|
|
553
|
+
const { package_name } = args;
|
|
554
|
+
if (!package_name) {
|
|
555
|
+
return { content: [{ type: 'text', text: 'Error: package_name is required' }] };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(package_name)}`, {
|
|
560
|
+
signal: AbortSignal.timeout(10_000),
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
if (res.status === 404) {
|
|
564
|
+
return { content: [{ type: 'text', text: `⚠️ Package "${package_name}" is NOT in the AgentAudit security registry — it has never been audited.\n\nIMPORTANT: Tell the user that this package has no security audit on record and that installing unaudited packages carries risk. Recommend running a security audit first before installing.\n\nTo run an audit now:\n1. Find the source URL (GitHub repo) for "${package_name}"\n2. Call audit_package with that URL\n3. Analyze the code following the methodology\n4. Call submit_report with your findings\n\nIf the user wants to skip the audit and install anyway, warn them that the package is unvetted.` }] };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const data = await res.json();
|
|
568
|
+
const risk = data.risk_score ?? data.latest_risk_score ?? 'unknown';
|
|
569
|
+
const official = data.has_official_audit ? '✅ Officially audited' : 'Community audit';
|
|
570
|
+
const findings = data.total_findings ?? 0;
|
|
571
|
+
const maxSev = data.latest_max_severity;
|
|
572
|
+
const auditedAt = data.last_audited_at ? new Date(data.last_audited_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : 'unknown';
|
|
573
|
+
const version = data.latest_known_version || null;
|
|
574
|
+
const isOutdated = data.is_outdated;
|
|
575
|
+
|
|
576
|
+
// Build response with risk assessment FIRST so the agent sees it immediately
|
|
577
|
+
let summary = '';
|
|
578
|
+
|
|
579
|
+
// Risk assessment at the very top — this is what the agent needs to act on
|
|
580
|
+
if (typeof risk === 'number' && risk >= 40) {
|
|
581
|
+
summary += `🚨 UNSAFE — DO NOT INSTALL WITHOUT WARNING THE USER!\nRisk Score: ${risk}/100 | ${findings} finding(s)\n\nYou MUST tell the user: "${package_name}" has been flagged as HIGH RISK in a security audit. It has ${findings} security finding(s) and a risk score of ${risk}/100. Installing this package is NOT recommended. Ask the user if they want to proceed despite the risk, and link them to the full report: ${REGISTRY_URL}/skills/${package_name}\n\n`;
|
|
582
|
+
} else if (typeof risk === 'number' && risk >= 20) {
|
|
583
|
+
summary += `⚠️ MODERATE RISK — Inform the user before installing.\nRisk Score: ${risk}/100 | ${findings} finding(s)\n\nTell the user: "${package_name}" has some security findings (risk ${risk}/100). Recommend they review the report before installing: ${REGISTRY_URL}/skills/${package_name}\n\n`;
|
|
584
|
+
} else if (typeof risk === 'number') {
|
|
585
|
+
summary += `✅ LOW RISK — Safe to install.\nRisk Score: ${risk}/100 | ${findings} finding(s)\n\nThis package has been audited and appears safe. You can proceed with installation.\n\n`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (isOutdated) {
|
|
589
|
+
summary += `⚠️ OUTDATED: The package has changed since the last audit. Consider running a fresh audit first.\n\n`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Details section
|
|
593
|
+
summary += `--- Details ---\n`;
|
|
594
|
+
summary += `Package: ${package_name}\n`;
|
|
595
|
+
summary += `Status: ${official}\n`;
|
|
596
|
+
summary += `Last Audited: ${auditedAt}\n`;
|
|
597
|
+
if (version) summary += `Audited Version: ${version}\n`;
|
|
598
|
+
if (data.source_url) summary += `Source: ${data.source_url}\n`;
|
|
599
|
+
summary += `Registry: ${REGISTRY_URL}/skills/${package_name}\n`;
|
|
600
|
+
|
|
601
|
+
return { content: [{ type: 'text', text: summary }] };
|
|
602
|
+
} catch (err) {
|
|
603
|
+
return { content: [{ type: 'text', text: `Registry lookup failed: ${err.message}` }] };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
default:
|
|
608
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}. Available: discover_servers, audit_package, submit_report, check_package` }] };
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// ── Start ────────────────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
const transport = new StdioServerTransport();
|
|
615
|
+
await server.connect(transport);
|