agentaudit 3.9.28 → 3.9.30
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 +2302 -2251
- package/index.mjs +49 -1
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -1,2251 +1,2302 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* AgentAudit CLI — Security scanner for AI tools
|
|
4
|
-
*
|
|
5
|
-
* Scan & Audit: scan <url>, audit <url>, discover
|
|
6
|
-
* Registry: check <name|url>, lookup <name>
|
|
7
|
-
* Setup: status, setup, config
|
|
8
|
-
*
|
|
9
|
-
* Global flags: --json, --quiet, --no-color, --provider, --debug, --export
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from 'fs';
|
|
13
|
-
import os from 'os';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import { execSync } from 'child_process';
|
|
16
|
-
import { createInterface } from 'readline';
|
|
17
|
-
import { fileURLToPath } from 'url';
|
|
18
|
-
|
|
19
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const SKILL_DIR = path.resolve(__dirname);
|
|
21
|
-
const REGISTRY_URL = 'https://agentaudit.dev';
|
|
22
|
-
|
|
23
|
-
// ── Provider resolution ────
|
|
24
|
-
function resolveProvider(flagOverride, keys) {
|
|
25
|
-
const orModel = process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
26
|
-
const ollamaModel = process.env.OLLAMA_MODEL || 'llama3.1';
|
|
27
|
-
const ollamaHost = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
28
|
-
const customUrl = process.env.LLM_API_URL;
|
|
29
|
-
const customKey = process.env.LLM_API_KEY;
|
|
30
|
-
const customModel = process.env.LLM_MODEL || 'default';
|
|
31
|
-
|
|
32
|
-
const providers = {
|
|
33
|
-
anthropic: keys.anthropicKey ? { id: 'anthropic', label: 'Anthropic (Claude)', key: keys.anthropicKey } : null,
|
|
34
|
-
openai: keys.openaiKey ? { id: 'openai', label: 'OpenAI (GPT-4o)', key: keys.openaiKey } : null,
|
|
35
|
-
openrouter: keys.openrouterKey ? { id: 'openrouter', label: `OpenRouter (${orModel})`, key: keys.openrouterKey } : null,
|
|
36
|
-
ollama: process.env.OLLAMA_MODEL || process.env.OLLAMA_HOST ? { id: 'ollama', label: `Ollama (${ollamaModel})`, key: null, host: ollamaHost, model: ollamaModel } : null,
|
|
37
|
-
custom: customUrl ? { id: 'custom', label: `Custom (${customModel})`, key: customKey, url: customUrl, model: customModel } : null,
|
|
38
|
-
};
|
|
39
|
-
// Aliases
|
|
40
|
-
const aliases = { claude: 'anthropic', gpt: 'openai', 'gpt-4o': 'openai', 'gpt4': 'openai', or: 'openrouter', local: 'ollama' };
|
|
41
|
-
|
|
42
|
-
// Priority: --provider flag > AGENTAUDIT_PROVIDER env > config file > model-inferred > auto-detect
|
|
43
|
-
const preferred = flagOverride
|
|
44
|
-
|| process.env.AGENTAUDIT_PROVIDER?.toLowerCase()
|
|
45
|
-
|| loadConfig()?.preferred_provider
|
|
46
|
-
|| null;
|
|
47
|
-
|
|
48
|
-
if (preferred) {
|
|
49
|
-
const resolved = aliases[preferred] || preferred;
|
|
50
|
-
const p = providers[resolved];
|
|
51
|
-
if (!p) return null;
|
|
52
|
-
return p;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Smart inference: if model is set, try to match it to a provider
|
|
56
|
-
const activeModel = globalModelOverride || process.env.AGENTAUDIT_MODEL || loadConfig()?.preferred_model;
|
|
57
|
-
if (activeModel) {
|
|
58
|
-
const lm = activeModel.toLowerCase();
|
|
59
|
-
// Direct provider models (no slash = native format)
|
|
60
|
-
if (!lm.includes('/')) {
|
|
61
|
-
if (lm.startsWith('claude') && providers.anthropic) return providers.anthropic;
|
|
62
|
-
if ((lm.startsWith('gpt') || lm.startsWith('o3') || lm.startsWith('o4') || lm.startsWith('o1')) && providers.openai) return providers.openai;
|
|
63
|
-
if (providers.ollama && (process.env.OLLAMA_MODEL || process.env.OLLAMA_HOST)) return providers.ollama;
|
|
64
|
-
}
|
|
65
|
-
// Slash format = OpenRouter convention (provider/model)
|
|
66
|
-
if (lm.includes('/') && providers.openrouter) return providers.openrouter;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Auto-detect priority: Anthropic > OpenAI > OpenRouter > Custom > Ollama (local last — usually weaker)
|
|
70
|
-
return providers.anthropic || providers.openai || providers.openrouter || providers.custom || providers.ollama || null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Global flags (set in main before command routing) ────
|
|
74
|
-
let jsonMode = false;
|
|
75
|
-
let quietMode = false;
|
|
76
|
-
let modelOverride = null; // --model flag or AGENTAUDIT_MODEL env or config
|
|
77
|
-
let globalModelOverride = null; // same, but set early for resolveProvider
|
|
78
|
-
|
|
79
|
-
// ── ANSI Colors (respects NO_COLOR and --no-color) ───────
|
|
80
|
-
|
|
81
|
-
const noColor = !!(process.env.NO_COLOR || process.argv.includes('--no-color'));
|
|
82
|
-
|
|
83
|
-
const c = noColor ? {
|
|
84
|
-
reset: '', bold: '', dim: '', red: '', green: '', yellow: '',
|
|
85
|
-
blue: '', magenta: '', cyan: '', white: '', gray: '',
|
|
86
|
-
bgRed: '', bgGreen: '', bgYellow: '',
|
|
87
|
-
} : {
|
|
88
|
-
reset: '\x1b[0m',
|
|
89
|
-
bold: '\x1b[1m',
|
|
90
|
-
dim: '\x1b[2m',
|
|
91
|
-
red: '\x1b[31m',
|
|
92
|
-
green: '\x1b[32m',
|
|
93
|
-
yellow: '\x1b[33m',
|
|
94
|
-
blue: '\x1b[34m',
|
|
95
|
-
magenta: '\x1b[35m',
|
|
96
|
-
cyan: '\x1b[36m',
|
|
97
|
-
white: '\x1b[37m',
|
|
98
|
-
gray: '\x1b[90m',
|
|
99
|
-
bgRed: '\x1b[41m',
|
|
100
|
-
bgGreen: '\x1b[42m',
|
|
101
|
-
bgYellow: '\x1b[43m',
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const icons = {
|
|
105
|
-
safe: `${c.green}✔${c.reset}`,
|
|
106
|
-
caution: `${c.yellow}⚠${c.reset}`,
|
|
107
|
-
unsafe: `${c.red}✖${c.reset}`,
|
|
108
|
-
info: `${c.blue}ℹ${c.reset}`,
|
|
109
|
-
scan: `${c.cyan}◉${c.reset}`,
|
|
110
|
-
tree: `${c.gray}├──${c.reset}`,
|
|
111
|
-
treeLast: `${c.gray}└──${c.reset}`,
|
|
112
|
-
pipe: `${c.gray}│${c.reset}`,
|
|
113
|
-
bullet: `${c.gray}•${c.reset}`,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// ── Credentials ─────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
119
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
120
|
-
const USER_CRED_DIR = path.join(xdgConfig, 'agentaudit');
|
|
121
|
-
const USER_CRED_FILE = path.join(USER_CRED_DIR, 'credentials.json');
|
|
122
|
-
const SKILL_CRED_FILE = path.join(SKILL_DIR, 'config', 'credentials.json');
|
|
123
|
-
|
|
124
|
-
function loadCredentials() {
|
|
125
|
-
for (const f of [SKILL_CRED_FILE, USER_CRED_FILE]) {
|
|
126
|
-
if (fs.existsSync(f)) {
|
|
127
|
-
try {
|
|
128
|
-
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
129
|
-
if (data.api_key) return data;
|
|
130
|
-
} catch {}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (process.env.AGENTAUDIT_API_KEY) {
|
|
134
|
-
return { api_key: process.env.AGENTAUDIT_API_KEY, agent_name: 'env' };
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function saveCredentials(data) {
|
|
140
|
-
const json = JSON.stringify(data, null, 2);
|
|
141
|
-
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
142
|
-
fs.writeFileSync(USER_CRED_FILE, json, { mode: 0o600 });
|
|
143
|
-
try {
|
|
144
|
-
fs.mkdirSync(path.dirname(SKILL_CRED_FILE), { recursive: true });
|
|
145
|
-
fs.writeFileSync(SKILL_CRED_FILE, json, { mode: 0o600 });
|
|
146
|
-
} catch {}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const USER_CONFIG_FILE = path.join(USER_CRED_DIR, 'config.json');
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
console.log(` ${icons.safe}
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (
|
|
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
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
);
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const sc = severityColor(
|
|
787
|
-
console.log(
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}));
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
//
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
console.log(
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
console.log();
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
console.log(
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
console.log(
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
});
|
|
1381
|
-
console.log(` ${c.
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
console.log(`
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
console.log(
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
console.log(` ${
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
console.log();
|
|
1770
|
-
console.log(`
|
|
1771
|
-
console.log(`
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
//
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
||
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
console.log(
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
console.log(`
|
|
1887
|
-
console.log();
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
};
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
const
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
if (
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
const
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
//
|
|
2012
|
-
console.log(
|
|
2013
|
-
console.log(`
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
console.log(`
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
if (
|
|
2218
|
-
console.log(
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentAudit CLI — Security scanner for AI tools
|
|
4
|
+
*
|
|
5
|
+
* Scan & Audit: scan <url>, audit <url>, discover
|
|
6
|
+
* Registry: check <name|url>, lookup <name>
|
|
7
|
+
* Setup: status, setup, config
|
|
8
|
+
*
|
|
9
|
+
* Global flags: --json, --quiet, --no-color, --provider, --debug, --export
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { createInterface } from 'readline';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const SKILL_DIR = path.resolve(__dirname);
|
|
21
|
+
const REGISTRY_URL = 'https://agentaudit.dev';
|
|
22
|
+
|
|
23
|
+
// ── Provider resolution ────
|
|
24
|
+
function resolveProvider(flagOverride, keys) {
|
|
25
|
+
const orModel = process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
26
|
+
const ollamaModel = process.env.OLLAMA_MODEL || 'llama3.1';
|
|
27
|
+
const ollamaHost = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
28
|
+
const customUrl = process.env.LLM_API_URL;
|
|
29
|
+
const customKey = process.env.LLM_API_KEY;
|
|
30
|
+
const customModel = process.env.LLM_MODEL || 'default';
|
|
31
|
+
|
|
32
|
+
const providers = {
|
|
33
|
+
anthropic: keys.anthropicKey ? { id: 'anthropic', label: 'Anthropic (Claude)', key: keys.anthropicKey } : null,
|
|
34
|
+
openai: keys.openaiKey ? { id: 'openai', label: 'OpenAI (GPT-4o)', key: keys.openaiKey } : null,
|
|
35
|
+
openrouter: keys.openrouterKey ? { id: 'openrouter', label: `OpenRouter (${orModel})`, key: keys.openrouterKey } : null,
|
|
36
|
+
ollama: process.env.OLLAMA_MODEL || process.env.OLLAMA_HOST ? { id: 'ollama', label: `Ollama (${ollamaModel})`, key: null, host: ollamaHost, model: ollamaModel } : null,
|
|
37
|
+
custom: customUrl ? { id: 'custom', label: `Custom (${customModel})`, key: customKey, url: customUrl, model: customModel } : null,
|
|
38
|
+
};
|
|
39
|
+
// Aliases
|
|
40
|
+
const aliases = { claude: 'anthropic', gpt: 'openai', 'gpt-4o': 'openai', 'gpt4': 'openai', or: 'openrouter', local: 'ollama' };
|
|
41
|
+
|
|
42
|
+
// Priority: --provider flag > AGENTAUDIT_PROVIDER env > config file > model-inferred > auto-detect
|
|
43
|
+
const preferred = flagOverride
|
|
44
|
+
|| process.env.AGENTAUDIT_PROVIDER?.toLowerCase()
|
|
45
|
+
|| loadConfig()?.preferred_provider
|
|
46
|
+
|| null;
|
|
47
|
+
|
|
48
|
+
if (preferred) {
|
|
49
|
+
const resolved = aliases[preferred] || preferred;
|
|
50
|
+
const p = providers[resolved];
|
|
51
|
+
if (!p) return null;
|
|
52
|
+
return p;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Smart inference: if model is set, try to match it to a provider
|
|
56
|
+
const activeModel = globalModelOverride || process.env.AGENTAUDIT_MODEL || loadConfig()?.preferred_model;
|
|
57
|
+
if (activeModel) {
|
|
58
|
+
const lm = activeModel.toLowerCase();
|
|
59
|
+
// Direct provider models (no slash = native format)
|
|
60
|
+
if (!lm.includes('/')) {
|
|
61
|
+
if (lm.startsWith('claude') && providers.anthropic) return providers.anthropic;
|
|
62
|
+
if ((lm.startsWith('gpt') || lm.startsWith('o3') || lm.startsWith('o4') || lm.startsWith('o1')) && providers.openai) return providers.openai;
|
|
63
|
+
if (providers.ollama && (process.env.OLLAMA_MODEL || process.env.OLLAMA_HOST)) return providers.ollama;
|
|
64
|
+
}
|
|
65
|
+
// Slash format = OpenRouter convention (provider/model)
|
|
66
|
+
if (lm.includes('/') && providers.openrouter) return providers.openrouter;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Auto-detect priority: Anthropic > OpenAI > OpenRouter > Custom > Ollama (local last — usually weaker)
|
|
70
|
+
return providers.anthropic || providers.openai || providers.openrouter || providers.custom || providers.ollama || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Global flags (set in main before command routing) ────
|
|
74
|
+
let jsonMode = false;
|
|
75
|
+
let quietMode = false;
|
|
76
|
+
let modelOverride = null; // --model flag or AGENTAUDIT_MODEL env or config
|
|
77
|
+
let globalModelOverride = null; // same, but set early for resolveProvider
|
|
78
|
+
|
|
79
|
+
// ── ANSI Colors (respects NO_COLOR and --no-color) ───────
|
|
80
|
+
|
|
81
|
+
const noColor = !!(process.env.NO_COLOR || process.argv.includes('--no-color'));
|
|
82
|
+
|
|
83
|
+
const c = noColor ? {
|
|
84
|
+
reset: '', bold: '', dim: '', red: '', green: '', yellow: '',
|
|
85
|
+
blue: '', magenta: '', cyan: '', white: '', gray: '',
|
|
86
|
+
bgRed: '', bgGreen: '', bgYellow: '',
|
|
87
|
+
} : {
|
|
88
|
+
reset: '\x1b[0m',
|
|
89
|
+
bold: '\x1b[1m',
|
|
90
|
+
dim: '\x1b[2m',
|
|
91
|
+
red: '\x1b[31m',
|
|
92
|
+
green: '\x1b[32m',
|
|
93
|
+
yellow: '\x1b[33m',
|
|
94
|
+
blue: '\x1b[34m',
|
|
95
|
+
magenta: '\x1b[35m',
|
|
96
|
+
cyan: '\x1b[36m',
|
|
97
|
+
white: '\x1b[37m',
|
|
98
|
+
gray: '\x1b[90m',
|
|
99
|
+
bgRed: '\x1b[41m',
|
|
100
|
+
bgGreen: '\x1b[42m',
|
|
101
|
+
bgYellow: '\x1b[43m',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const icons = {
|
|
105
|
+
safe: `${c.green}✔${c.reset}`,
|
|
106
|
+
caution: `${c.yellow}⚠${c.reset}`,
|
|
107
|
+
unsafe: `${c.red}✖${c.reset}`,
|
|
108
|
+
info: `${c.blue}ℹ${c.reset}`,
|
|
109
|
+
scan: `${c.cyan}◉${c.reset}`,
|
|
110
|
+
tree: `${c.gray}├──${c.reset}`,
|
|
111
|
+
treeLast: `${c.gray}└──${c.reset}`,
|
|
112
|
+
pipe: `${c.gray}│${c.reset}`,
|
|
113
|
+
bullet: `${c.gray}•${c.reset}`,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ── Credentials ─────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
119
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
120
|
+
const USER_CRED_DIR = path.join(xdgConfig, 'agentaudit');
|
|
121
|
+
const USER_CRED_FILE = path.join(USER_CRED_DIR, 'credentials.json');
|
|
122
|
+
const SKILL_CRED_FILE = path.join(SKILL_DIR, 'config', 'credentials.json');
|
|
123
|
+
|
|
124
|
+
function loadCredentials() {
|
|
125
|
+
for (const f of [SKILL_CRED_FILE, USER_CRED_FILE]) {
|
|
126
|
+
if (fs.existsSync(f)) {
|
|
127
|
+
try {
|
|
128
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
129
|
+
if (data.api_key) return data;
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (process.env.AGENTAUDIT_API_KEY) {
|
|
134
|
+
return { api_key: process.env.AGENTAUDIT_API_KEY, agent_name: 'env' };
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function saveCredentials(data) {
|
|
140
|
+
const json = JSON.stringify(data, null, 2);
|
|
141
|
+
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
142
|
+
fs.writeFileSync(USER_CRED_FILE, json, { mode: 0o600 });
|
|
143
|
+
try {
|
|
144
|
+
fs.mkdirSync(path.dirname(SKILL_CRED_FILE), { recursive: true });
|
|
145
|
+
fs.writeFileSync(SKILL_CRED_FILE, json, { mode: 0o600 });
|
|
146
|
+
} catch {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const USER_CONFIG_FILE = path.join(USER_CRED_DIR, 'config.json');
|
|
150
|
+
const USER_STATS_FILE = path.join(USER_CRED_DIR, 'stats-cache.json');
|
|
151
|
+
|
|
152
|
+
function loadStatsCache() {
|
|
153
|
+
try { return fs.existsSync(USER_STATS_FILE) ? JSON.parse(fs.readFileSync(USER_STATS_FILE, 'utf8')) : null; } catch { return null; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function saveStatsCache(stats) {
|
|
157
|
+
try { fs.mkdirSync(USER_CRED_DIR, { recursive: true }); fs.writeFileSync(USER_STATS_FILE, JSON.stringify({ ...stats, _ts: Date.now() })); } catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function refreshStatsCache(agentName) {
|
|
161
|
+
try {
|
|
162
|
+
const lbRes = await fetch(`${REGISTRY_URL}/api/leaderboard`, { signal: AbortSignal.timeout(5000) });
|
|
163
|
+
if (!lbRes.ok) return null;
|
|
164
|
+
const agents = await lbRes.json();
|
|
165
|
+
const idx = Array.isArray(agents) ? agents.findIndex(a => (a.agent_name || '').toLowerCase() === agentName.toLowerCase()) : -1;
|
|
166
|
+
if (idx < 0) return null;
|
|
167
|
+
const me = agents[idx];
|
|
168
|
+
const stats = { rank: idx + 1, total: agents.length, pts: me.total_points || 0, reports: me.total_reports || 0, official: !!me.is_official };
|
|
169
|
+
saveStatsCache(stats);
|
|
170
|
+
return stats;
|
|
171
|
+
} catch { return null; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function loadConfig() {
|
|
175
|
+
try {
|
|
176
|
+
if (fs.existsSync(USER_CONFIG_FILE)) {
|
|
177
|
+
return JSON.parse(fs.readFileSync(USER_CONFIG_FILE, 'utf8'));
|
|
178
|
+
}
|
|
179
|
+
} catch {}
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function saveConfig(data) {
|
|
184
|
+
const existing = loadConfig();
|
|
185
|
+
const merged = { ...existing, ...data };
|
|
186
|
+
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
187
|
+
fs.writeFileSync(USER_CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function askQuestion(question) {
|
|
191
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
192
|
+
return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer.trim()); }));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Interactive multi-select in terminal. No dependencies.
|
|
197
|
+
* items: [{ label, sublabel?, value, checked? }]
|
|
198
|
+
* Returns: array of selected values
|
|
199
|
+
*/
|
|
200
|
+
function multiSelect(items, { title = 'Select items', hint = 'Space=toggle ↑↓=move a=all n=none Enter=confirm' } = {}) {
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
if (!process.stdin.isTTY) {
|
|
203
|
+
// Non-interactive: return all items
|
|
204
|
+
resolve(items.map(i => i.value));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const selected = new Set(items.filter(i => i.checked).map((_, idx) => idx));
|
|
209
|
+
let cursor = 0;
|
|
210
|
+
|
|
211
|
+
const render = () => {
|
|
212
|
+
// Move cursor up to overwrite previous render
|
|
213
|
+
process.stdout.write(`\x1b[${items.length + 3}A\x1b[J`);
|
|
214
|
+
draw();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const draw = () => {
|
|
218
|
+
console.log(` ${c.bold}${title}${c.reset} ${c.dim}(${selected.size}/${items.length} selected)${c.reset}`);
|
|
219
|
+
console.log(` ${c.dim}${hint}${c.reset}`);
|
|
220
|
+
console.log();
|
|
221
|
+
for (let i = 0; i < items.length; i++) {
|
|
222
|
+
const item = items[i];
|
|
223
|
+
const isCursor = i === cursor;
|
|
224
|
+
const isSelected = selected.has(i);
|
|
225
|
+
const pointer = isCursor ? `${c.cyan}❯${c.reset}` : ' ';
|
|
226
|
+
const checkbox = isSelected ? `${c.green}◉${c.reset}` : `${c.dim}○${c.reset}`;
|
|
227
|
+
const label = isCursor ? `${c.bold}${item.label}${c.reset}` : item.label;
|
|
228
|
+
const sub = item.sublabel ? ` ${c.dim}${item.sublabel}${c.reset}` : '';
|
|
229
|
+
console.log(` ${pointer} ${checkbox} ${label}${sub}`);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Initial draw
|
|
234
|
+
draw();
|
|
235
|
+
|
|
236
|
+
process.stdin.setRawMode(true);
|
|
237
|
+
process.stdin.resume();
|
|
238
|
+
process.stdin.setEncoding('utf8');
|
|
239
|
+
|
|
240
|
+
const onData = (key) => {
|
|
241
|
+
// Ctrl+C
|
|
242
|
+
if (key === '\x03') {
|
|
243
|
+
process.stdin.setRawMode(false);
|
|
244
|
+
process.stdin.pause();
|
|
245
|
+
process.stdin.removeListener('data', onData);
|
|
246
|
+
console.log();
|
|
247
|
+
process.exitCode = 0; return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Enter
|
|
251
|
+
if (key === '\r' || key === '\n') {
|
|
252
|
+
process.stdin.setRawMode(false);
|
|
253
|
+
process.stdin.pause();
|
|
254
|
+
process.stdin.removeListener('data', onData);
|
|
255
|
+
resolve(items.filter((_, i) => selected.has(i)).map(i => i.value));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Space — toggle
|
|
260
|
+
if (key === ' ') {
|
|
261
|
+
if (selected.has(cursor)) selected.delete(cursor);
|
|
262
|
+
else selected.add(cursor);
|
|
263
|
+
render();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// a — select all
|
|
268
|
+
if (key === 'a') {
|
|
269
|
+
for (let i = 0; i < items.length; i++) selected.add(i);
|
|
270
|
+
render();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// n — select none
|
|
275
|
+
if (key === 'n') {
|
|
276
|
+
selected.clear();
|
|
277
|
+
render();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Arrow up / k
|
|
282
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
283
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
284
|
+
render();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Arrow down / j
|
|
289
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
290
|
+
cursor = (cursor + 1) % items.length;
|
|
291
|
+
render();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
process.stdin.on('data', onData);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function registerAgent(agentName) {
|
|
301
|
+
const res = await fetch(`${REGISTRY_URL}/api/register`, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: { 'Content-Type': 'application/json' },
|
|
304
|
+
body: JSON.stringify({ agent_name: agentName }),
|
|
305
|
+
signal: AbortSignal.timeout(15_000),
|
|
306
|
+
});
|
|
307
|
+
if (!res.ok) throw new Error(`Registration failed (HTTP ${res.status}): ${await res.text()}`);
|
|
308
|
+
return res.json();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function setupCommand() {
|
|
312
|
+
console.log(` ${c.bold}Setup${c.reset}`);
|
|
313
|
+
console.log();
|
|
314
|
+
|
|
315
|
+
const existing = loadCredentials();
|
|
316
|
+
if (existing) {
|
|
317
|
+
console.log(` ${icons.safe} Already configured as ${c.bold}${existing.agent_name}${c.reset}`);
|
|
318
|
+
console.log(` ${c.dim}Key: ${existing.api_key.slice(0, 8)}...${c.reset}`);
|
|
319
|
+
console.log();
|
|
320
|
+
const answer = await askQuestion(` Reconfigure? ${c.dim}(y/N)${c.reset} `);
|
|
321
|
+
if (answer.toLowerCase() !== 'y') {
|
|
322
|
+
console.log(` ${c.dim}Keeping existing config.${c.reset}`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
console.log();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(` ${c.bold}1)${c.reset} Register new agent ${c.dim}(free, creates API key automatically)${c.reset}`);
|
|
329
|
+
console.log(` ${c.bold}2)${c.reset} Enter existing API key`);
|
|
330
|
+
console.log();
|
|
331
|
+
const choice = await askQuestion(` Choice ${c.dim}(1/2)${c.reset}: `);
|
|
332
|
+
console.log();
|
|
333
|
+
|
|
334
|
+
if (choice === '2') {
|
|
335
|
+
const key = await askQuestion(` API Key: `);
|
|
336
|
+
if (!key) { console.log(` ${c.red}No key entered.${c.reset}`); return; }
|
|
337
|
+
const name = await askQuestion(` Agent name ${c.dim}(optional)${c.reset}: `);
|
|
338
|
+
saveCredentials({ api_key: key, agent_name: name || 'custom' });
|
|
339
|
+
console.log();
|
|
340
|
+
console.log(` ${icons.safe} Saved! Key stored in ${c.dim}${USER_CRED_FILE}${c.reset}`);
|
|
341
|
+
} else {
|
|
342
|
+
const name = await askQuestion(` Agent name ${c.dim}(e.g. my-scanner, claude-desktop)${c.reset}: `);
|
|
343
|
+
if (!name || !/^[a-zA-Z0-9._-]{2,64}$/.test(name)) {
|
|
344
|
+
console.log(` ${c.red}Invalid name. Use 2-64 chars: letters, numbers, dash, underscore, dot.${c.reset}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
process.stdout.write(` Registering ${c.bold}${name}${c.reset}...`);
|
|
348
|
+
try {
|
|
349
|
+
const data = await registerAgent(name);
|
|
350
|
+
saveCredentials({ api_key: data.api_key, agent_name: data.agent_name });
|
|
351
|
+
console.log(` ${c.green}done!${c.reset}`);
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(` ${icons.safe} Registered as ${c.bold}${data.agent_name}${c.reset}`);
|
|
354
|
+
console.log(` ${c.dim}Key: ${data.api_key.slice(0, 12)}...${c.reset}`);
|
|
355
|
+
console.log(` ${c.dim}Saved to: ${USER_CRED_FILE}${c.reset}`);
|
|
356
|
+
// Initialize stats cache
|
|
357
|
+
refreshStatsCache(data.agent_name).catch(() => {});
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
360
|
+
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(` ${c.bold}Ready!${c.reset} You can now:`);
|
|
367
|
+
console.log(` ${c.dim}•${c.reset} Discover servers: ${c.cyan}agentaudit discover${c.reset}`);
|
|
368
|
+
console.log(` ${c.dim}•${c.reset} Audit packages: ${c.cyan}agentaudit audit <repo-url>${c.reset} ${c.dim}(deep LLM analysis)${c.reset}`);
|
|
369
|
+
console.log(` ${c.dim}•${c.reset} Quick scan: ${c.cyan}agentaudit scan <repo-url>${c.reset} ${c.dim}(regex-based)${c.reset}`);
|
|
370
|
+
console.log(` ${c.dim}•${c.reset} Check registry: ${c.cyan}agentaudit check <name>${c.reset}`);
|
|
371
|
+
console.log(` ${c.dim}•${c.reset} Submit reports via MCP in Claude/Cursor/Windsurf`);
|
|
372
|
+
console.log();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Structured error output ─────────────────────────────
|
|
376
|
+
|
|
377
|
+
function emitError(code, message, hint, exitCode = 2) {
|
|
378
|
+
if (jsonMode) {
|
|
379
|
+
process.stderr.write(JSON.stringify({ error: true, code, message, hint: hint || undefined, exitCode }) + '\n');
|
|
380
|
+
}
|
|
381
|
+
process.exitCode = exitCode;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Levenshtein distance for typo correction ────────────
|
|
385
|
+
|
|
386
|
+
function levenshtein(a, b) {
|
|
387
|
+
const m = a.length, n = b.length;
|
|
388
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
389
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
390
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
391
|
+
for (let i = 1; i <= m; i++)
|
|
392
|
+
for (let j = 1; j <= n; j++)
|
|
393
|
+
dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + (a[i-1] !== b[j-1] ? 1 : 0));
|
|
394
|
+
return dp[m][n];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
function getVersion() {
|
|
400
|
+
try {
|
|
401
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
402
|
+
return pkg.version || '0.0.0';
|
|
403
|
+
} catch { return '0.0.0'; }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function banner() {
|
|
407
|
+
if (quietMode || jsonMode) return;
|
|
408
|
+
const creds = loadCredentials();
|
|
409
|
+
const agentInfo = creds?.agent_name ? ` ${c.dim}·${c.reset} ${c.green}${creds.agent_name}${c.reset}` : '';
|
|
410
|
+
console.log();
|
|
411
|
+
console.log(` 🛡 ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}${agentInfo}`);
|
|
412
|
+
// Show cached stats inline
|
|
413
|
+
if (creds?.agent_name) {
|
|
414
|
+
const cached = loadStatsCache();
|
|
415
|
+
if (cached && cached.pts !== undefined) {
|
|
416
|
+
const medal = cached.rank === 1 ? '🥇' : cached.rank === 2 ? '🥈' : cached.rank === 3 ? '🥉' : `#${cached.rank}`;
|
|
417
|
+
console.log(` ${c.dim}${medal} · ${cached.pts} pts · ${cached.reports} reports${cached.official ? ` · ${c.green}Official${c.reset}${c.dim}` : ''}${c.reset}`);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
console.log(` ${c.dim}Security scanner for AI tools${c.reset}`);
|
|
421
|
+
}
|
|
422
|
+
console.log();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function slugFromUrl(url) {
|
|
426
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/.\s]+)/);
|
|
427
|
+
if (match) {
|
|
428
|
+
const owner = match[1].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
429
|
+
const repo = match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
430
|
+
// Generic repo names get owner prefix to avoid collisions
|
|
431
|
+
const generic = ['mcp', 'server', 'plugin', 'tool', 'agent', 'sdk', 'api', 'app', 'cli', 'lib', 'core'];
|
|
432
|
+
if (generic.includes(repo)) return `${owner}-${repo}`;
|
|
433
|
+
return repo;
|
|
434
|
+
}
|
|
435
|
+
return url.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function elapsed(startMs) {
|
|
439
|
+
const ms = Date.now() - startMs;
|
|
440
|
+
if (ms < 1000) return `${ms}ms`;
|
|
441
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function riskBadge(score) {
|
|
445
|
+
if (score === 0) return `${c.bgGreen}${c.bold}${c.white} SAFE ${c.reset}`;
|
|
446
|
+
if (score <= 10) return `${c.bgGreen}${c.white} LOW ${c.reset}`;
|
|
447
|
+
if (score <= 30) return `${c.bgYellow}${c.bold} CAUTION ${c.reset}`;
|
|
448
|
+
return `${c.bgRed}${c.bold}${c.white} UNSAFE ${c.reset}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function severityColor(sev) {
|
|
452
|
+
switch (sev) {
|
|
453
|
+
case 'critical': return c.red;
|
|
454
|
+
case 'high': return c.red;
|
|
455
|
+
case 'medium': return c.yellow;
|
|
456
|
+
case 'low': return c.blue;
|
|
457
|
+
default: return c.gray;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function severityIcon(sev) {
|
|
462
|
+
switch (sev) {
|
|
463
|
+
case 'critical': return `${c.red}●${c.reset}`;
|
|
464
|
+
case 'high': return `${c.red}●${c.reset}`;
|
|
465
|
+
case 'medium': return `${c.yellow}●${c.reset}`;
|
|
466
|
+
case 'low': return `${c.blue}●${c.reset}`;
|
|
467
|
+
default: return `${c.green}●${c.reset}`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── File Collection (same logic as MCP server) ──────────
|
|
472
|
+
|
|
473
|
+
function extractJSON(text) {
|
|
474
|
+
// 1. Try parsing the entire text as JSON directly
|
|
475
|
+
try { return JSON.parse(text.trim()); } catch {}
|
|
476
|
+
|
|
477
|
+
// 2. Strip markdown code fences — try last fence first (report is usually at the end)
|
|
478
|
+
const fenceMatches = [...text.matchAll(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/g)];
|
|
479
|
+
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
480
|
+
try {
|
|
481
|
+
const parsed = JSON.parse(fenceMatches[i][1].trim());
|
|
482
|
+
if (parsed && typeof parsed === 'object' && ('risk_score' in parsed || 'findings' in parsed || 'result' in parsed)) return parsed;
|
|
483
|
+
} catch {}
|
|
484
|
+
}
|
|
485
|
+
// Try any fence even without report keys
|
|
486
|
+
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
487
|
+
try { return JSON.parse(fenceMatches[i][1].trim()); } catch {}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 3. Find ALL balanced top-level { ... } blocks, try each (prefer largest valid one)
|
|
491
|
+
const blocks = [];
|
|
492
|
+
let searchFrom = 0;
|
|
493
|
+
while (searchFrom < text.length) {
|
|
494
|
+
const start = text.indexOf('{', searchFrom);
|
|
495
|
+
if (start === -1) break;
|
|
496
|
+
let depth = 0, inStr = false, esc = false;
|
|
497
|
+
let end = -1;
|
|
498
|
+
for (let i = start; i < text.length; i++) {
|
|
499
|
+
const ch = text[i];
|
|
500
|
+
if (esc) { esc = false; continue; }
|
|
501
|
+
if (ch === '\\' && inStr) { esc = true; continue; }
|
|
502
|
+
if (ch === '"') { inStr = !inStr; continue; }
|
|
503
|
+
if (inStr) continue;
|
|
504
|
+
if (ch === '{') depth++;
|
|
505
|
+
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
506
|
+
}
|
|
507
|
+
if (end > start) {
|
|
508
|
+
blocks.push(text.slice(start, end + 1));
|
|
509
|
+
searchFrom = end + 1;
|
|
510
|
+
} else {
|
|
511
|
+
searchFrom = start + 1;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Try largest block first (the report JSON is usually the biggest)
|
|
515
|
+
blocks.sort((a, b) => b.length - a.length);
|
|
516
|
+
for (const block of blocks) {
|
|
517
|
+
try { return JSON.parse(block); } catch {}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const MAX_FILE_SIZE = 50_000;
|
|
524
|
+
const MAX_TOTAL_SIZE = 300_000;
|
|
525
|
+
const SKIP_DIRS = new Set([
|
|
526
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
527
|
+
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
528
|
+
'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
|
|
529
|
+
'examples', 'example', 'fixtures', '.github', '.vscode', '.idea',
|
|
530
|
+
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
531
|
+
]);
|
|
532
|
+
const SKIP_EXTENSIONS = new Set([
|
|
533
|
+
'.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff',
|
|
534
|
+
'.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.tar', '.gz',
|
|
535
|
+
'.map', '.min.js', '.min.css', '.d.ts', '.pyc', '.pyo', '.so',
|
|
536
|
+
'.dylib', '.dll', '.exe', '.bin', '.dat', '.db', '.sqlite',
|
|
537
|
+
]);
|
|
538
|
+
|
|
539
|
+
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
540
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
541
|
+
let entries;
|
|
542
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
543
|
+
catch { return collected; }
|
|
544
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
545
|
+
for (const entry of entries) {
|
|
546
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
|
|
547
|
+
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
548
|
+
const fullPath = path.join(dir, entry.name);
|
|
549
|
+
if (entry.isDirectory()) {
|
|
550
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
551
|
+
collectFiles(fullPath, relPath, collected, totalSize);
|
|
552
|
+
} else {
|
|
553
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
554
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
555
|
+
try {
|
|
556
|
+
const stat = fs.statSync(fullPath);
|
|
557
|
+
if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
|
|
558
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
559
|
+
totalSize.bytes += content.length;
|
|
560
|
+
collected.push({ path: relPath, content, size: stat.size });
|
|
561
|
+
} catch {}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return collected;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── Detect package properties ───────────────────────────
|
|
568
|
+
|
|
569
|
+
function detectPackageInfo(repoPath, files) {
|
|
570
|
+
const info = { type: 'unknown', tools: [], prompts: [], language: 'unknown', entrypoint: null };
|
|
571
|
+
|
|
572
|
+
// Detect language
|
|
573
|
+
const exts = files.map(f => path.extname(f.path).toLowerCase());
|
|
574
|
+
const extCounts = {};
|
|
575
|
+
exts.forEach(e => { extCounts[e] = (extCounts[e] || 0) + 1; });
|
|
576
|
+
const topExt = Object.entries(extCounts).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
577
|
+
|
|
578
|
+
const langMap = { '.py': 'Python', '.js': 'JavaScript', '.ts': 'TypeScript', '.mjs': 'JavaScript', '.rs': 'Rust', '.go': 'Go', '.java': 'Java', '.rb': 'Ruby' };
|
|
579
|
+
info.language = langMap[topExt] || topExt || 'unknown';
|
|
580
|
+
|
|
581
|
+
// Detect package type
|
|
582
|
+
const allContent = files.map(f => f.content).join('\n');
|
|
583
|
+
if (allContent.includes('@modelcontextprotocol') || allContent.includes('FastMCP') || allContent.includes('mcp.server') || allContent.includes('mcp_server')) {
|
|
584
|
+
info.type = 'mcp-server';
|
|
585
|
+
} else if (files.some(f => f.path.toLowerCase() === 'skill.md')) {
|
|
586
|
+
info.type = 'agent-skill';
|
|
587
|
+
} else if (allContent.includes('#!/usr/bin/env') || allContent.includes('argparse') || allContent.includes('commander')) {
|
|
588
|
+
info.type = 'cli-tool';
|
|
589
|
+
} else {
|
|
590
|
+
info.type = 'library';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Extract MCP tools (look for tool definitions)
|
|
594
|
+
const toolPatterns = [
|
|
595
|
+
// JS/TS: name: 'tool_name' or "tool_name" in tool definitions
|
|
596
|
+
/(?:name|tool_name)['":\s]+['"]([a-z_][a-z0-9_]*)['"]/gi,
|
|
597
|
+
// Python: @mcp.tool() def func_name or Tool(name="...")
|
|
598
|
+
/(?:@(?:mcp|server)\.tool\(\)[\s\S]*?def\s+([a-z_][a-z0-9_]*))|(?:Tool\s*\(\s*name\s*=\s*['"]([a-z_][a-z0-9_]*)['"])/gi,
|
|
599
|
+
// Direct: tool names in ListTools handlers
|
|
600
|
+
/['"]name['"]\s*:\s*['"]([a-z_][a-z0-9_]*)['"]/gi,
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
const toolSet = new Set();
|
|
604
|
+
for (const file of files) {
|
|
605
|
+
for (const pattern of toolPatterns) {
|
|
606
|
+
pattern.lastIndex = 0;
|
|
607
|
+
let m;
|
|
608
|
+
while ((m = pattern.exec(file.content)) !== null) {
|
|
609
|
+
const name = m[1] || m[2];
|
|
610
|
+
if (name && name.length > 2 && name.length < 50 && !['type', 'name', 'string', 'object', 'number', 'boolean', 'array', 'required', 'description', 'default', 'null', 'true', 'false', 'none'].includes(name)) {
|
|
611
|
+
toolSet.add(name);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
info.tools = [...toolSet];
|
|
617
|
+
|
|
618
|
+
// Extract prompts (look for prompt definitions)
|
|
619
|
+
const promptPatterns = [
|
|
620
|
+
/(?:prompt|PROMPT)['":\s]+['"]([a-z_][a-z0-9_]*)['"]/gi,
|
|
621
|
+
/@(?:mcp|server)\.prompt\(\)[\s\S]*?def\s+([a-z_][a-z0-9_]*)/gi,
|
|
622
|
+
];
|
|
623
|
+
const promptSet = new Set();
|
|
624
|
+
for (const file of files) {
|
|
625
|
+
for (const pattern of promptPatterns) {
|
|
626
|
+
pattern.lastIndex = 0;
|
|
627
|
+
let m;
|
|
628
|
+
while ((m = pattern.exec(file.content)) !== null) {
|
|
629
|
+
if (m[1] && m[1].length > 2) promptSet.add(m[1]);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
info.prompts = [...promptSet];
|
|
634
|
+
|
|
635
|
+
// Detect entrypoint
|
|
636
|
+
const entryFiles = ['index.js', 'index.ts', 'index.mjs', 'main.py', 'server.py', 'app.py', 'src/index.ts', 'src/main.ts', 'src/index.js'];
|
|
637
|
+
for (const ef of entryFiles) {
|
|
638
|
+
if (files.some(f => f.path === ef)) { info.entrypoint = ef; break; }
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return info;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ── Quick static checks ─────────────────────────────────
|
|
645
|
+
|
|
646
|
+
function quickChecks(files) {
|
|
647
|
+
const findings = [];
|
|
648
|
+
|
|
649
|
+
const checks = [
|
|
650
|
+
{
|
|
651
|
+
id: 'EXEC_INJECTION',
|
|
652
|
+
title: 'Command injection risk',
|
|
653
|
+
severity: 'high',
|
|
654
|
+
pattern: /(?:exec(?:Sync)?|spawn|child_process|subprocess|os\.system|os\.popen|Popen)\s*\([^)]*(?:\$\{|`|\+\s*(?:req|input|args|param|user|query))/i,
|
|
655
|
+
category: 'injection',
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: 'EVAL_USAGE',
|
|
659
|
+
title: 'Dynamic code evaluation',
|
|
660
|
+
severity: 'high',
|
|
661
|
+
pattern: /(?:^|[^a-z])eval\s*\([^)]*(?:input|req|user|param|arg|query)/im,
|
|
662
|
+
category: 'injection',
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: 'HARDCODED_SECRET',
|
|
666
|
+
title: 'Potential hardcoded secret',
|
|
667
|
+
severity: 'medium',
|
|
668
|
+
pattern: /(?:api[_-]?key|password|secret|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{16,}['"]/i,
|
|
669
|
+
category: 'secrets',
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
id: 'SSL_DISABLED',
|
|
673
|
+
title: 'SSL/TLS verification disabled',
|
|
674
|
+
severity: 'medium',
|
|
675
|
+
pattern: /(?:rejectUnauthorized\s*:\s*false|verify\s*=\s*False|VERIFY_SSL\s*=\s*false|NODE_TLS_REJECT_UNAUTHORIZED|InsecureRequestWarning)/i,
|
|
676
|
+
category: 'crypto',
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
id: 'PATH_TRAVERSAL',
|
|
680
|
+
title: 'Potential path traversal',
|
|
681
|
+
severity: 'medium',
|
|
682
|
+
pattern: /(?:\.\.\/|\.\.\\|path\.join|os\.path\.join)\s*\([^)]*(?:input|req|user|param|arg|query)/i,
|
|
683
|
+
category: 'filesystem',
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: 'CORS_WILDCARD',
|
|
687
|
+
title: 'Wildcard CORS origin',
|
|
688
|
+
severity: 'low',
|
|
689
|
+
pattern: /(?:Access-Control-Allow-Origin|cors)\s*[:({]\s*['"]\*/i,
|
|
690
|
+
category: 'network',
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
id: 'TELEMETRY',
|
|
694
|
+
title: 'Undisclosed telemetry',
|
|
695
|
+
severity: 'low',
|
|
696
|
+
pattern: /(?:posthog|mixpanel|analytics|telemetry|tracking|sentry).*(?:init|setup|track|capture)/i,
|
|
697
|
+
category: 'privacy',
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
id: 'SHELL_EXEC',
|
|
701
|
+
title: 'Shell command execution',
|
|
702
|
+
severity: 'high',
|
|
703
|
+
pattern: /(?:subprocess\.(?:run|call|Popen)|os\.system|os\.popen|execSync|child_process\.exec)\s*\(/i,
|
|
704
|
+
category: 'injection',
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
id: 'SQL_INJECTION',
|
|
708
|
+
title: 'Potential SQL injection',
|
|
709
|
+
severity: 'high',
|
|
710
|
+
pattern: /(?:execute|query|raw)\s*\(\s*(?:f['"]|['"].*?%s|['"].*?\{|['"].*?\+)/i,
|
|
711
|
+
category: 'injection',
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
id: 'YAML_UNSAFE',
|
|
715
|
+
title: 'Unsafe YAML loading',
|
|
716
|
+
severity: 'medium',
|
|
717
|
+
pattern: /yaml\.(?:load|unsafe_load)\s*\(/i,
|
|
718
|
+
category: 'deserialization',
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 'PICKLE_LOAD',
|
|
722
|
+
title: 'Unsafe deserialization (pickle)',
|
|
723
|
+
severity: 'high',
|
|
724
|
+
pattern: /pickle\.loads?\s*\(/i,
|
|
725
|
+
category: 'deserialization',
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: 'PROMPT_INJECTION',
|
|
729
|
+
title: 'Prompt injection vector',
|
|
730
|
+
severity: 'high',
|
|
731
|
+
pattern: /(?:<IMPORTANT>|<SYSTEM>|ignore previous|you are now|new instructions)/i,
|
|
732
|
+
category: 'prompt-injection',
|
|
733
|
+
},
|
|
734
|
+
];
|
|
735
|
+
|
|
736
|
+
for (const file of files) {
|
|
737
|
+
for (const check of checks) {
|
|
738
|
+
const match = check.pattern.exec(file.content);
|
|
739
|
+
if (match) {
|
|
740
|
+
// Find line number
|
|
741
|
+
const lines = file.content.slice(0, match.index).split('\n');
|
|
742
|
+
findings.push({
|
|
743
|
+
...check,
|
|
744
|
+
file: file.path,
|
|
745
|
+
line: lines.length,
|
|
746
|
+
snippet: match[0].trim().slice(0, 80),
|
|
747
|
+
confidence: 'medium',
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return findings;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ── Registry check ──────────────────────────────────────
|
|
757
|
+
|
|
758
|
+
async function checkRegistry(slug) {
|
|
759
|
+
try {
|
|
760
|
+
const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
|
|
761
|
+
signal: AbortSignal.timeout(5000),
|
|
762
|
+
});
|
|
763
|
+
if (res.ok) return await res.json();
|
|
764
|
+
} catch {}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ── Print results ───────────────────────────────────────
|
|
769
|
+
|
|
770
|
+
function printScanResult(url, info, files, findings, registryData, duration) {
|
|
771
|
+
if (jsonMode) return; // JSON mode handles output separately
|
|
772
|
+
|
|
773
|
+
const slug = slugFromUrl(url);
|
|
774
|
+
|
|
775
|
+
// Quiet mode: compact one-line-per-package output
|
|
776
|
+
if (quietMode) {
|
|
777
|
+
if (findings.length > 0) {
|
|
778
|
+
const bySev = {};
|
|
779
|
+
for (const f of findings) { bySev[f.severity] = (bySev[f.severity] || 0) + 1; }
|
|
780
|
+
const sevStr = Object.entries(bySev).map(([s, n]) => {
|
|
781
|
+
const sc = severityColor(s);
|
|
782
|
+
return `${sc}${n} ${s}${c.reset}`;
|
|
783
|
+
}).join(', ');
|
|
784
|
+
console.log(`${icons.caution} ${c.bold}${slug}${c.reset} ${findings.length} findings (${sevStr}) ${c.dim}${duration}${c.reset}`);
|
|
785
|
+
for (const f of findings) {
|
|
786
|
+
const sc = severityColor(f.severity);
|
|
787
|
+
console.log(` ${severityIcon(f.severity)} ${sc}${f.severity.toUpperCase().padEnd(8)}${c.reset} ${f.title} ${c.dim}${f.file}:${f.line}${c.reset}`);
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
console.log(`${icons.safe} ${c.bold}${slug}${c.reset} ${c.green}clean${c.reset} ${c.dim}${files.length} files, ${duration}${c.reset}`);
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Header
|
|
796
|
+
console.log(`${icons.scan} ${c.bold}${slug}${c.reset} ${c.dim}${url}${c.reset}`);
|
|
797
|
+
console.log(`${icons.pipe} ${c.dim}${info.language} ${info.type}${c.reset} ${c.dim}${files.length} files scanned in ${duration}${c.reset}`);
|
|
798
|
+
|
|
799
|
+
// Tools & prompts tree
|
|
800
|
+
const items = [
|
|
801
|
+
...info.tools.map(t => ({ kind: 'tool', name: t })),
|
|
802
|
+
...info.prompts.map(p => ({ kind: 'prompt', name: p })),
|
|
803
|
+
];
|
|
804
|
+
|
|
805
|
+
if (items.length > 0) {
|
|
806
|
+
console.log(`${icons.pipe}`);
|
|
807
|
+
for (let i = 0; i < items.length; i++) {
|
|
808
|
+
const isLast = i === items.length - 1 && findings.length === 0;
|
|
809
|
+
const branch = isLast ? icons.treeLast : icons.tree;
|
|
810
|
+
const item = items[i];
|
|
811
|
+
const kindLabel = item.kind === 'tool' ? `${c.dim}tool${c.reset} ` : `${c.dim}prompt${c.reset}`;
|
|
812
|
+
const padName = item.name.padEnd(28);
|
|
813
|
+
|
|
814
|
+
// Check if this tool has a finding associated
|
|
815
|
+
const toolFinding = findings.find(f =>
|
|
816
|
+
f.snippet && f.snippet.toLowerCase().includes(item.name.toLowerCase())
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
if (toolFinding) {
|
|
820
|
+
const sc = severityColor(toolFinding.severity);
|
|
821
|
+
console.log(`${branch} ${kindLabel} ${c.bold}${padName}${c.reset} ${sc}⚠ flagged${c.reset} — ${toolFinding.title}`);
|
|
822
|
+
} else {
|
|
823
|
+
console.log(`${branch} ${kindLabel} ${c.bold}${padName}${c.reset} ${c.green}✔ ok${c.reset}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
console.log(`${icons.pipe} ${c.dim}(no tools or prompts detected)${c.reset}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Findings
|
|
831
|
+
if (findings.length > 0) {
|
|
832
|
+
console.log(`${icons.pipe}`);
|
|
833
|
+
console.log(`${icons.pipe} ${c.bold}Findings (${findings.length})${c.reset} ${c.dim}static analysis — may include false positives${c.reset}`);
|
|
834
|
+
for (let i = 0; i < findings.length; i++) {
|
|
835
|
+
const f = findings[i];
|
|
836
|
+
const isLast = i === findings.length - 1;
|
|
837
|
+
const branch = isLast ? icons.treeLast : icons.tree;
|
|
838
|
+
const pipeOrSpace = isLast ? ' ' : `${icons.pipe} `;
|
|
839
|
+
const sc = severityColor(f.severity);
|
|
840
|
+
console.log(`${branch} ${severityIcon(f.severity)} ${sc}${f.severity.toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
|
|
841
|
+
console.log(`${pipeOrSpace} ${c.dim}${f.file}:${f.line}${c.reset} ${c.dim}${f.snippet || ''}${c.reset}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Registry status
|
|
846
|
+
console.log(`${icons.pipe}`);
|
|
847
|
+
if (registryData) {
|
|
848
|
+
const rd = registryData;
|
|
849
|
+
const riskScore = rd.risk_score ?? rd.latest_risk_score ?? 0;
|
|
850
|
+
console.log(`${icons.treeLast} ${c.dim}registry${c.reset} ${riskBadge(riskScore)} Risk ${riskScore} ${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
|
|
851
|
+
} else {
|
|
852
|
+
console.log(`${icons.treeLast} ${c.dim}registry${c.reset} ${c.dim}not audited yet${c.reset}`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
console.log();
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function printSummary(results) {
|
|
859
|
+
const total = results.length;
|
|
860
|
+
const safe = results.filter(r => r.findings.length === 0).length;
|
|
861
|
+
const withFindings = total - safe;
|
|
862
|
+
const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
863
|
+
|
|
864
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
865
|
+
console.log(` ${c.bold}Summary${c.reset} ${total} packages scanned`);
|
|
866
|
+
console.log();
|
|
867
|
+
if (safe > 0) console.log(` ${icons.safe} ${c.green}${safe} clean${c.reset}`);
|
|
868
|
+
if (withFindings > 0) console.log(` ${icons.caution} ${c.yellow}${withFindings} with findings${c.reset} (${totalFindings} total)`);
|
|
869
|
+
|
|
870
|
+
// Breakdown by severity
|
|
871
|
+
const bySev = {};
|
|
872
|
+
results.forEach(r => r.findings.forEach(f => {
|
|
873
|
+
bySev[f.severity] = (bySev[f.severity] || 0) + 1;
|
|
874
|
+
}));
|
|
875
|
+
if (Object.keys(bySev).length > 0) {
|
|
876
|
+
console.log();
|
|
877
|
+
for (const sev of ['critical', 'high', 'medium', 'low']) {
|
|
878
|
+
if (bySev[sev]) {
|
|
879
|
+
console.log(` ${severityIcon(sev)} ${bySev[sev]}× ${severityColor(sev)}${sev}${c.reset}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
console.log();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ── Clone & Scan ────────────────────────────────────────
|
|
888
|
+
|
|
889
|
+
async function scanRepo(url) {
|
|
890
|
+
const start = Date.now();
|
|
891
|
+
const slug = slugFromUrl(url);
|
|
892
|
+
|
|
893
|
+
if (!jsonMode) process.stdout.write(`${icons.scan} Scanning ${c.bold}${slug}${c.reset} ${c.dim}...${c.reset}`);
|
|
894
|
+
|
|
895
|
+
// Clone
|
|
896
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
897
|
+
const repoPath = path.join(tmpDir, 'repo');
|
|
898
|
+
try {
|
|
899
|
+
execSync(`git clone --depth 1 "${url}" "${repoPath}"`, {
|
|
900
|
+
timeout: 30_000,
|
|
901
|
+
stdio: 'pipe',
|
|
902
|
+
});
|
|
903
|
+
} catch (err) {
|
|
904
|
+
if (!jsonMode) {
|
|
905
|
+
process.stdout.write(` ${c.red}✖ clone failed${c.reset}\n`);
|
|
906
|
+
const msg = err.stderr?.toString().trim() || err.message?.split('\n')[0] || '';
|
|
907
|
+
if (msg) console.log(` ${c.dim}${msg}${c.reset}`);
|
|
908
|
+
console.log(` ${c.dim}Make sure git is installed and the URL is accessible.${c.reset}`);
|
|
909
|
+
}
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Collect files
|
|
914
|
+
const files = collectFiles(repoPath);
|
|
915
|
+
|
|
916
|
+
// Detect info
|
|
917
|
+
const info = detectPackageInfo(repoPath, files);
|
|
918
|
+
|
|
919
|
+
// Quick checks
|
|
920
|
+
const findings = quickChecks(files);
|
|
921
|
+
|
|
922
|
+
// Registry lookup
|
|
923
|
+
const registryData = await checkRegistry(slug);
|
|
924
|
+
|
|
925
|
+
// Cleanup
|
|
926
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
927
|
+
|
|
928
|
+
const duration = elapsed(start);
|
|
929
|
+
|
|
930
|
+
if (!jsonMode) {
|
|
931
|
+
// Clear the "Scanning..." line
|
|
932
|
+
process.stdout.write('\r\x1b[K');
|
|
933
|
+
|
|
934
|
+
// Print result
|
|
935
|
+
printScanResult(url, info, files, findings, registryData, duration);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return { slug, url, info, files: files.length, findings, registryData, duration };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ── Discover local MCP configs ──────────────────────────
|
|
942
|
+
|
|
943
|
+
function findMcpConfigs() {
|
|
944
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
945
|
+
const platform = process.platform;
|
|
946
|
+
|
|
947
|
+
// All known MCP config locations
|
|
948
|
+
const candidates = [
|
|
949
|
+
// Claude Desktop
|
|
950
|
+
{ name: 'Claude Desktop', path: path.join(home, '.claude', 'mcp.json') },
|
|
951
|
+
{ name: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
|
|
952
|
+
{ name: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
|
|
953
|
+
{ name: 'Claude Desktop', path: path.join(home, '.config', 'claude', 'claude_desktop_config.json') },
|
|
954
|
+
// Cursor
|
|
955
|
+
{ name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json') },
|
|
956
|
+
// Windsurf / Codeium
|
|
957
|
+
{ name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
|
|
958
|
+
// VS Code
|
|
959
|
+
{ name: 'VS Code', path: path.join(home, '.vscode', 'mcp.json') },
|
|
960
|
+
// Continue.dev
|
|
961
|
+
{ name: 'Continue', path: path.join(home, '.continue', 'config.json') },
|
|
962
|
+
];
|
|
963
|
+
|
|
964
|
+
// Also check AGENTAUDIT_TEST_CONFIG env for testing
|
|
965
|
+
if (process.env.AGENTAUDIT_TEST_CONFIG) {
|
|
966
|
+
candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG });
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Also scan workspace .cursor/mcp.json, .vscode/mcp.json in cwd
|
|
970
|
+
const cwd = process.cwd();
|
|
971
|
+
candidates.push(
|
|
972
|
+
{ name: 'Cursor (project)', path: path.join(cwd, '.cursor', 'mcp.json') },
|
|
973
|
+
{ name: 'VS Code (project)', path: path.join(cwd, '.vscode', 'mcp.json') },
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
const found = [];
|
|
977
|
+
for (const c of candidates) {
|
|
978
|
+
if (fs.existsSync(c.path)) {
|
|
979
|
+
try {
|
|
980
|
+
const content = JSON.parse(fs.readFileSync(c.path, 'utf8'));
|
|
981
|
+
found.push({ ...c, content });
|
|
982
|
+
} catch {}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return found;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function extractServersFromConfig(config) {
|
|
989
|
+
// Handle both { mcpServers: {...} } and { servers: {...} } formats
|
|
990
|
+
const servers = config.mcpServers || config.servers || {};
|
|
991
|
+
const result = [];
|
|
992
|
+
|
|
993
|
+
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
994
|
+
const info = {
|
|
995
|
+
name,
|
|
996
|
+
command: serverConfig.command || null,
|
|
997
|
+
args: serverConfig.args || [],
|
|
998
|
+
url: serverConfig.url || null,
|
|
999
|
+
sourceUrl: null,
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// Try to extract source URL from args (common patterns)
|
|
1003
|
+
const allArgs = [info.command, ...info.args].filter(Boolean).join(' ');
|
|
1004
|
+
|
|
1005
|
+
// npx package-name → npm package
|
|
1006
|
+
const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
|
|
1007
|
+
if (npxMatch) info.npmPackage = npxMatch[1];
|
|
1008
|
+
|
|
1009
|
+
// node /path/to/something → try to find package.json
|
|
1010
|
+
const nodePathMatch = allArgs.match(/node\s+["']?([^"'\s]+)/);
|
|
1011
|
+
if (nodePathMatch) {
|
|
1012
|
+
const scriptPath = nodePathMatch[1];
|
|
1013
|
+
// Walk up to find package.json with repository
|
|
1014
|
+
let dir = path.dirname(path.resolve(scriptPath));
|
|
1015
|
+
for (let i = 0; i < 5; i++) {
|
|
1016
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
1017
|
+
if (fs.existsSync(pkgPath)) {
|
|
1018
|
+
try {
|
|
1019
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1020
|
+
if (pkg.repository?.url) {
|
|
1021
|
+
info.sourceUrl = pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
1022
|
+
}
|
|
1023
|
+
if (pkg.name) info.npmPackage = pkg.name;
|
|
1024
|
+
} catch {}
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
const parent = path.dirname(dir);
|
|
1028
|
+
if (parent === dir) break;
|
|
1029
|
+
dir = parent;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// python/uvx with package name
|
|
1034
|
+
const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
|
|
1035
|
+
if (pyMatch) info.pyPackage = pyMatch[1];
|
|
1036
|
+
|
|
1037
|
+
// URL-based MCP server (remote HTTP)
|
|
1038
|
+
if (info.url && !info.npmPackage && !info.pyPackage) {
|
|
1039
|
+
try {
|
|
1040
|
+
const parsed = new URL(info.url);
|
|
1041
|
+
// Extract service name from hostname: mcp.supabase.com → supabase
|
|
1042
|
+
const hostParts = parsed.hostname.split('.');
|
|
1043
|
+
if (hostParts.length >= 2) {
|
|
1044
|
+
const serviceName = hostParts.length === 3 ? hostParts[1] : hostParts[0];
|
|
1045
|
+
info.remoteService = serviceName;
|
|
1046
|
+
}
|
|
1047
|
+
} catch {}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
result.push(info);
|
|
1051
|
+
}
|
|
1052
|
+
return result;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function serverSlug(server) {
|
|
1056
|
+
// Try to derive a slug for registry lookup
|
|
1057
|
+
if (server.npmPackage) return server.npmPackage.replace(/^@/, '').replace(/\//g, '-');
|
|
1058
|
+
if (server.pyPackage) return server.pyPackage.replace(/[^a-z0-9-]/gi, '-');
|
|
1059
|
+
return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function searchGitHub(query) {
|
|
1063
|
+
try {
|
|
1064
|
+
const res = await fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=1`, {
|
|
1065
|
+
signal: AbortSignal.timeout(5000),
|
|
1066
|
+
headers: { 'Accept': 'application/vnd.github+json' },
|
|
1067
|
+
});
|
|
1068
|
+
if (res.ok) {
|
|
1069
|
+
const data = await res.json();
|
|
1070
|
+
if (data.items?.length > 0) {
|
|
1071
|
+
return data.items[0].html_url;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
} catch {}
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async function resolveSourceUrl(server) {
|
|
1079
|
+
// Already have it
|
|
1080
|
+
if (server.sourceUrl) return server.sourceUrl;
|
|
1081
|
+
|
|
1082
|
+
// Try npm registry
|
|
1083
|
+
if (server.npmPackage) {
|
|
1084
|
+
try {
|
|
1085
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npmPackage)}`, {
|
|
1086
|
+
signal: AbortSignal.timeout(5000),
|
|
1087
|
+
});
|
|
1088
|
+
if (res.ok) {
|
|
1089
|
+
const data = await res.json();
|
|
1090
|
+
let repoUrl = data.repository?.url;
|
|
1091
|
+
if (repoUrl) {
|
|
1092
|
+
repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
|
|
1093
|
+
if (repoUrl.startsWith('http')) return repoUrl;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
} catch {}
|
|
1097
|
+
// Fallback: try GitHub search for the package name
|
|
1098
|
+
const ghUrl = await searchGitHub(server.npmPackage);
|
|
1099
|
+
if (ghUrl) return ghUrl;
|
|
1100
|
+
return `https://www.npmjs.com/package/${server.npmPackage}`;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Try PyPI
|
|
1104
|
+
if (server.pyPackage) {
|
|
1105
|
+
try {
|
|
1106
|
+
const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pyPackage)}/json`, {
|
|
1107
|
+
signal: AbortSignal.timeout(5000),
|
|
1108
|
+
});
|
|
1109
|
+
if (res.ok) {
|
|
1110
|
+
const data = await res.json();
|
|
1111
|
+
const urls = data.info?.project_urls || {};
|
|
1112
|
+
const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
|
|
1113
|
+
if (source && source.startsWith('http')) return source;
|
|
1114
|
+
}
|
|
1115
|
+
} catch {}
|
|
1116
|
+
// Fallback: GitHub search
|
|
1117
|
+
const ghUrl = await searchGitHub(server.pyPackage);
|
|
1118
|
+
if (ghUrl) return ghUrl;
|
|
1119
|
+
return `https://pypi.org/project/${server.pyPackage}/`;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// URL-based remote MCP server — try GitHub search by service name
|
|
1123
|
+
if (server.remoteService) {
|
|
1124
|
+
// Try npm registry with common MCP naming patterns
|
|
1125
|
+
for (const tryName of [
|
|
1126
|
+
`@${server.remoteService}/mcp-server-${server.remoteService}`,
|
|
1127
|
+
`${server.remoteService}-mcp`,
|
|
1128
|
+
`mcp-server-${server.remoteService}`,
|
|
1129
|
+
server.remoteService,
|
|
1130
|
+
]) {
|
|
1131
|
+
try {
|
|
1132
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
|
|
1133
|
+
signal: AbortSignal.timeout(3000),
|
|
1134
|
+
});
|
|
1135
|
+
if (res.ok) {
|
|
1136
|
+
const data = await res.json();
|
|
1137
|
+
let repoUrl = data.repository?.url;
|
|
1138
|
+
if (repoUrl) {
|
|
1139
|
+
repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
|
|
1140
|
+
if (repoUrl.startsWith('http')) return repoUrl;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
} catch {}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Last resort: if server has a url, show it as context
|
|
1148
|
+
if (server.url) {
|
|
1149
|
+
try {
|
|
1150
|
+
const parsed = new URL(server.url);
|
|
1151
|
+
return `https://github.com/search?q=${encodeURIComponent(parsed.hostname + ' MCP')}&type=repositories`;
|
|
1152
|
+
} catch {}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async function discoverCommand(options = {}) {
|
|
1159
|
+
const autoScan = options.scan || false;
|
|
1160
|
+
const interactiveAudit = options.audit || false;
|
|
1161
|
+
|
|
1162
|
+
if (!jsonMode) {
|
|
1163
|
+
console.log(` ${c.bold}Discovering MCP servers in your AI editors...${c.reset}`);
|
|
1164
|
+
console.log();
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const configs = findMcpConfigs();
|
|
1168
|
+
|
|
1169
|
+
if (configs.length === 0) {
|
|
1170
|
+
console.log(` ${c.yellow}No MCP configurations found.${c.reset}`);
|
|
1171
|
+
console.log(` ${c.dim}Searched: Claude Desktop, Cursor, Windsurf, VS Code${c.reset}`);
|
|
1172
|
+
console.log();
|
|
1173
|
+
console.log(` ${c.dim}MCP config locations:${c.reset}`);
|
|
1174
|
+
console.log(` ${c.dim} Claude: ~/.claude/mcp.json${c.reset}`);
|
|
1175
|
+
console.log(` ${c.dim} Cursor: ~/.cursor/mcp.json${c.reset}`);
|
|
1176
|
+
console.log(` ${c.dim} Windsurf: ~/.codeium/windsurf/mcp_config.json${c.reset}`);
|
|
1177
|
+
console.log(` ${c.dim} VS Code: ~/.vscode/mcp.json${c.reset}`);
|
|
1178
|
+
console.log();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
let totalServers = 0;
|
|
1183
|
+
let checkedServers = 0;
|
|
1184
|
+
let auditedServers = 0;
|
|
1185
|
+
let unauditedServers = 0;
|
|
1186
|
+
const unauditedWithUrls = [];
|
|
1187
|
+
const allServersWithUrls = []; // For --scan: all servers we can scan
|
|
1188
|
+
|
|
1189
|
+
for (const config of configs) {
|
|
1190
|
+
const servers = extractServersFromConfig(config.content);
|
|
1191
|
+
const serverCount = servers.length;
|
|
1192
|
+
totalServers += serverCount;
|
|
1193
|
+
|
|
1194
|
+
const countLabel = serverCount === 0
|
|
1195
|
+
? `${c.dim}no servers${c.reset}`
|
|
1196
|
+
: `found ${c.bold}${serverCount}${c.reset} server${serverCount > 1 ? 's' : ''}`;
|
|
1197
|
+
|
|
1198
|
+
console.log(`${icons.bullet} Scanning ${c.bold}${config.name}${c.reset} ${c.dim}${config.path}${c.reset} ${countLabel}`);
|
|
1199
|
+
|
|
1200
|
+
if (serverCount === 0) {
|
|
1201
|
+
console.log();
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
console.log();
|
|
1206
|
+
|
|
1207
|
+
for (let i = 0; i < servers.length; i++) {
|
|
1208
|
+
const server = servers[i];
|
|
1209
|
+
const isLast = i === servers.length - 1;
|
|
1210
|
+
const branch = isLast ? icons.treeLast : icons.tree;
|
|
1211
|
+
const pipe = isLast ? ' ' : `${icons.pipe} `;
|
|
1212
|
+
|
|
1213
|
+
const slug = serverSlug(server);
|
|
1214
|
+
checkedServers++;
|
|
1215
|
+
|
|
1216
|
+
// Registry lookup
|
|
1217
|
+
const registryData = await checkRegistry(slug);
|
|
1218
|
+
|
|
1219
|
+
// Also try with server name directly
|
|
1220
|
+
let regData = registryData;
|
|
1221
|
+
if (!regData && slug !== server.name.toLowerCase()) {
|
|
1222
|
+
regData = await checkRegistry(server.name.toLowerCase());
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Determine source display
|
|
1226
|
+
let sourceLabel = '';
|
|
1227
|
+
if (server.npmPackage) sourceLabel = `${c.dim}npm:${server.npmPackage}${c.reset}`;
|
|
1228
|
+
else if (server.pyPackage) sourceLabel = `${c.dim}pip:${server.pyPackage}${c.reset}`;
|
|
1229
|
+
else if (server.url) sourceLabel = `${c.dim}${server.url.length > 60 ? server.url.slice(0, 57) + '...' : server.url}${c.reset}`;
|
|
1230
|
+
else if (server.command) sourceLabel = `${c.dim}${[server.command, ...server.args.slice(0, 2)].join(' ')}${c.reset}`;
|
|
1231
|
+
|
|
1232
|
+
// Always resolve source URL (needed for --scan)
|
|
1233
|
+
const resolvedUrl = await resolveSourceUrl(server);
|
|
1234
|
+
|
|
1235
|
+
if (regData) {
|
|
1236
|
+
auditedServers++;
|
|
1237
|
+
const riskScore = regData.risk_score ?? regData.latest_risk_score ?? 0;
|
|
1238
|
+
const hasOfficial = regData.has_official_audit;
|
|
1239
|
+
console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
|
|
1240
|
+
console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
|
|
1241
|
+
if (resolvedUrl) allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: true, regData });
|
|
1242
|
+
} else {
|
|
1243
|
+
unauditedServers++;
|
|
1244
|
+
console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
|
|
1245
|
+
if (resolvedUrl) {
|
|
1246
|
+
console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: ${c.cyan}agentaudit audit ${resolvedUrl}${c.reset}`);
|
|
1247
|
+
unauditedWithUrls.push({ name: server.name, sourceUrl: resolvedUrl });
|
|
1248
|
+
allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: false });
|
|
1249
|
+
} else {
|
|
1250
|
+
console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Source URL unknown — check the package's GitHub/npm page${c.reset}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (server.sourceUrl && !server.sourceUrl.includes('npmjs.com')) {
|
|
1255
|
+
console.log(`${pipe} ${c.dim}source: ${server.sourceUrl}${c.reset}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
console.log();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Summary
|
|
1263
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1264
|
+
console.log(` ${c.bold}Summary${c.reset} ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`);
|
|
1265
|
+
console.log();
|
|
1266
|
+
if (auditedServers > 0) console.log(` ${icons.safe} ${c.green}${auditedServers} audited${c.reset}`);
|
|
1267
|
+
if (unauditedServers > 0) console.log(` ${icons.caution} ${c.yellow}${unauditedServers} not audited${c.reset}`);
|
|
1268
|
+
console.log();
|
|
1269
|
+
|
|
1270
|
+
// --scan: automatically scan all servers with resolved source URLs (git-cloneable only)
|
|
1271
|
+
if (autoScan) {
|
|
1272
|
+
const isCloneable = (url) => /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org)\//i.test(url);
|
|
1273
|
+
const scanTargets = allServersWithUrls.filter(s => s.sourceUrl && isCloneable(s.sourceUrl));
|
|
1274
|
+
// Deduplicate by sourceUrl
|
|
1275
|
+
const seen = new Set();
|
|
1276
|
+
const dedupedTargets = scanTargets.filter(s => {
|
|
1277
|
+
if (seen.has(s.sourceUrl)) return false;
|
|
1278
|
+
seen.add(s.sourceUrl);
|
|
1279
|
+
return true;
|
|
1280
|
+
});
|
|
1281
|
+
const skipped = allServersWithUrls.filter(s => s.sourceUrl && !isCloneable(s.sourceUrl));
|
|
1282
|
+
if (dedupedTargets.length > 0) {
|
|
1283
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1284
|
+
console.log(` ${c.bold}${icons.scan} Auto-scanning ${dedupedTargets.length} server${dedupedTargets.length !== 1 ? 's' : ''}...${c.reset}`);
|
|
1285
|
+
if (skipped.length > 0) {
|
|
1286
|
+
console.log(` ${c.dim}(${skipped.length} skipped — no cloneable source URL)${c.reset}`);
|
|
1287
|
+
}
|
|
1288
|
+
console.log();
|
|
1289
|
+
|
|
1290
|
+
const scanResults = [];
|
|
1291
|
+
for (const target of dedupedTargets) {
|
|
1292
|
+
const result = await scanRepo(target.sourceUrl);
|
|
1293
|
+
if (result) scanResults.push({ ...result, serverName: target.name });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (scanResults.length > 1) {
|
|
1297
|
+
// Print combined scan summary
|
|
1298
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1299
|
+
console.log(` ${c.bold}Scan Summary${c.reset} ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`);
|
|
1300
|
+
console.log();
|
|
1301
|
+
|
|
1302
|
+
let totalFindings = 0;
|
|
1303
|
+
let serversWithFindings = 0;
|
|
1304
|
+
|
|
1305
|
+
for (const r of scanResults) {
|
|
1306
|
+
const findingCount = r.findings ? r.findings.length : 0;
|
|
1307
|
+
totalFindings += findingCount;
|
|
1308
|
+
if (findingCount > 0) serversWithFindings++;
|
|
1309
|
+
|
|
1310
|
+
const status = findingCount === 0
|
|
1311
|
+
? `${icons.safe} ${c.green}clean${c.reset}`
|
|
1312
|
+
: `${icons.caution} ${c.yellow}${findingCount} finding${findingCount !== 1 ? 's' : ''}${c.reset}`;
|
|
1313
|
+
console.log(` ${status} ${c.bold}${r.serverName || r.slug}${c.reset} ${c.dim}(${r.duration})${c.reset}`);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
console.log();
|
|
1317
|
+
if (serversWithFindings > 0) {
|
|
1318
|
+
console.log(` ${c.yellow}${serversWithFindings}/${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} with findings (${totalFindings} total)${c.reset}`);
|
|
1319
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit scan <url> --deep${c.dim} for deep LLM analysis on flagged servers${c.reset}`);
|
|
1320
|
+
} else {
|
|
1321
|
+
console.log(` ${c.green}All servers passed quick scan${c.reset}`);
|
|
1322
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit scan <url> --deep${c.dim} for thorough LLM-powered analysis${c.reset}`);
|
|
1323
|
+
}
|
|
1324
|
+
console.log();
|
|
1325
|
+
}
|
|
1326
|
+
} else {
|
|
1327
|
+
console.log(` ${c.dim}No scannable source URLs found.${c.reset}`);
|
|
1328
|
+
console.log();
|
|
1329
|
+
}
|
|
1330
|
+
} else if (interactiveAudit && allServersWithUrls.length > 0) {
|
|
1331
|
+
// Interactive multi-select for audit
|
|
1332
|
+
const isCloneable = (url) => /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org)\//i.test(url);
|
|
1333
|
+
const auditCandidates = [];
|
|
1334
|
+
const seen = new Set();
|
|
1335
|
+
for (const s of allServersWithUrls) {
|
|
1336
|
+
if (!s.sourceUrl || !isCloneable(s.sourceUrl)) continue;
|
|
1337
|
+
if (seen.has(s.sourceUrl)) continue;
|
|
1338
|
+
seen.add(s.sourceUrl);
|
|
1339
|
+
auditCandidates.push(s);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (auditCandidates.length > 0) {
|
|
1343
|
+
console.log();
|
|
1344
|
+
const items = auditCandidates.map(s => ({
|
|
1345
|
+
label: s.name,
|
|
1346
|
+
sublabel: s.hasAudit ? `${c.green}✔ audited${c.reset} ${s.sourceUrl}` : s.sourceUrl,
|
|
1347
|
+
value: s,
|
|
1348
|
+
checked: !s.hasAudit, // Pre-select unaudited
|
|
1349
|
+
}));
|
|
1350
|
+
|
|
1351
|
+
const selected = await multiSelect(items, {
|
|
1352
|
+
title: 'Select servers to audit',
|
|
1353
|
+
hint: 'Space=toggle ↑↓=move a=all n=none Enter=confirm',
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
if (selected.length > 0) {
|
|
1357
|
+
console.log();
|
|
1358
|
+
console.log(` ${c.bold}Auditing ${selected.length} server${selected.length !== 1 ? 's' : ''}...${c.reset}`);
|
|
1359
|
+
console.log();
|
|
1360
|
+
for (const s of selected) {
|
|
1361
|
+
await auditRepo(s.sourceUrl);
|
|
1362
|
+
console.log();
|
|
1363
|
+
}
|
|
1364
|
+
} else {
|
|
1365
|
+
console.log();
|
|
1366
|
+
console.log(` ${c.dim}No servers selected.${c.reset}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
} else if (unauditedServers > 0) {
|
|
1370
|
+
if (unauditedWithUrls.length > 0) {
|
|
1371
|
+
console.log(` ${c.dim}To audit unaudited servers:${c.reset}`);
|
|
1372
|
+
for (const { name, sourceUrl } of unauditedWithUrls) {
|
|
1373
|
+
console.log(` ${c.cyan}agentaudit audit ${sourceUrl}${c.reset} ${c.dim}(${name})${c.reset}`);
|
|
1374
|
+
}
|
|
1375
|
+
} else {
|
|
1376
|
+
console.log(` ${c.dim}To audit unaudited servers, run:${c.reset}`);
|
|
1377
|
+
console.log(` ${c.cyan}agentaudit audit <source-url>${c.reset}`);
|
|
1378
|
+
}
|
|
1379
|
+
console.log();
|
|
1380
|
+
console.log(` ${c.dim}Or run ${c.cyan}agentaudit discover --quick${c.dim} to quick-scan all servers${c.reset}`);
|
|
1381
|
+
console.log(` ${c.dim}Or run ${c.cyan}agentaudit discover --deep${c.dim} to select & deep-audit interactively${c.reset}`);
|
|
1382
|
+
console.log();
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (!autoScan && !interactiveAudit && !jsonMode) {
|
|
1386
|
+
console.log(` ${c.dim}Looking for general package scanning? Try ${c.cyan}pip audit${c.dim} or ${c.cyan}npm audit${c.dim}.${c.reset}`);
|
|
1387
|
+
console.log();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// ── Audit command (deep LLM-powered) ────────────────────
|
|
1392
|
+
|
|
1393
|
+
function loadAuditPrompt() {
|
|
1394
|
+
const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
|
|
1395
|
+
if (fs.existsSync(promptPath)) return fs.readFileSync(promptPath, 'utf8');
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function auditRepo(url) {
|
|
1400
|
+
const start = Date.now();
|
|
1401
|
+
const slug = slugFromUrl(url);
|
|
1402
|
+
|
|
1403
|
+
console.log(`${icons.scan} ${c.bold}Auditing ${slug}${c.reset} ${c.dim}${url}${c.reset}`);
|
|
1404
|
+
console.log(`${icons.pipe} ${c.dim}Deep LLM-powered analysis (3-pass: UNDERSTAND → DETECT → CLASSIFY)${c.reset}`);
|
|
1405
|
+
console.log();
|
|
1406
|
+
|
|
1407
|
+
// Step 1: Clone
|
|
1408
|
+
process.stdout.write(` ${c.dim}[1/4]${c.reset} Cloning repository...`);
|
|
1409
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
1410
|
+
const repoPath = path.join(tmpDir, 'repo');
|
|
1411
|
+
try {
|
|
1412
|
+
execSync(`git clone --depth 1 "${url}" "${repoPath}"`, {
|
|
1413
|
+
timeout: 30_000, stdio: 'pipe',
|
|
1414
|
+
});
|
|
1415
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1418
|
+
const msg = err.stderr?.toString().trim() || err.message?.split('\n')[0] || '';
|
|
1419
|
+
if (msg) console.log(` ${c.dim}${msg}${c.reset}`);
|
|
1420
|
+
console.log(` ${c.dim}Make sure git is installed and the URL is accessible.${c.reset}`);
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Capture version + commit from cloned repo
|
|
1425
|
+
let repoCommitSha = null;
|
|
1426
|
+
let repoPackageVersion = null;
|
|
1427
|
+
try { repoCommitSha = execSync('git rev-parse HEAD', { cwd: repoPath, stdio: 'pipe' }).toString().trim(); } catch {}
|
|
1428
|
+
try {
|
|
1429
|
+
const pkgPath = path.join(repoPath, 'package.json');
|
|
1430
|
+
if (fs.existsSync(pkgPath)) {
|
|
1431
|
+
repoPackageVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version || null;
|
|
1432
|
+
}
|
|
1433
|
+
} catch {}
|
|
1434
|
+
|
|
1435
|
+
// Step 2: Collect files
|
|
1436
|
+
process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
|
|
1437
|
+
const files = collectFiles(repoPath);
|
|
1438
|
+
console.log(` ${c.green}${files.length} files${c.reset}`);
|
|
1439
|
+
|
|
1440
|
+
// Step 3: Build audit payload
|
|
1441
|
+
process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
|
|
1442
|
+
const auditPrompt = loadAuditPrompt();
|
|
1443
|
+
|
|
1444
|
+
let codeBlock = '';
|
|
1445
|
+
for (const file of files) {
|
|
1446
|
+
codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
1447
|
+
}
|
|
1448
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
1449
|
+
|
|
1450
|
+
// Step 4: LLM Analysis
|
|
1451
|
+
// Check for API keys to determine which LLM to use
|
|
1452
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
1453
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1454
|
+
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
|
1455
|
+
const openrouterModel = process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
1456
|
+
|
|
1457
|
+
// --provider flag overrides auto-detection
|
|
1458
|
+
const providerFlag = process.argv.find(a => a.startsWith('--provider='))?.split('=')[1]?.toLowerCase()
|
|
1459
|
+
|| (process.argv.includes('--provider') ? process.argv[process.argv.indexOf('--provider') + 1]?.toLowerCase() : null);
|
|
1460
|
+
|
|
1461
|
+
const resolvedProvider = resolveProvider(providerFlag, { anthropicKey, openaiKey, openrouterKey });
|
|
1462
|
+
const activeProvider = resolvedProvider?.label || null;
|
|
1463
|
+
|
|
1464
|
+
if (!resolvedProvider) {
|
|
1465
|
+
// No LLM API key — clear explanation
|
|
1466
|
+
console.log();
|
|
1467
|
+
console.log(` ${c.yellow}No LLM provider configured.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
|
|
1468
|
+
console.log();
|
|
1469
|
+
console.log(` ${c.bold}Option 1: Set an API key${c.reset} ${c.dim}(any one of these)${c.reset}`);
|
|
1470
|
+
console.log(` ${c.cyan}ANTHROPIC_API_KEY${c.reset} Anthropic Claude ${c.dim}(recommended)${c.reset}`);
|
|
1471
|
+
console.log(` ${c.cyan}OPENAI_API_KEY${c.reset} OpenAI GPT-4o`);
|
|
1472
|
+
console.log(` ${c.cyan}OPENROUTER_API_KEY${c.reset} OpenRouter ${c.dim}(200+ models)${c.reset}`);
|
|
1473
|
+
console.log(` ${c.cyan}OLLAMA_MODEL${c.reset} Ollama ${c.dim}(local, free, set model name)${c.reset}`);
|
|
1474
|
+
console.log(` ${c.cyan}LLM_API_URL${c.reset} Any OpenAI-compatible API ${c.dim}(+ LLM_API_KEY, LLM_MODEL)${c.reset}`);
|
|
1475
|
+
console.log();
|
|
1476
|
+
console.log(` ${c.dim}# Linux / macOS:${c.reset}`);
|
|
1477
|
+
console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
1478
|
+
console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
|
|
1479
|
+
console.log();
|
|
1480
|
+
console.log(` ${c.dim}# Windows (PowerShell):${c.reset}`);
|
|
1481
|
+
console.log(` ${c.dim}$env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
|
|
1482
|
+
console.log(` ${c.dim}$env:OPENAI_API_KEY = "sk-..."${c.reset}`);
|
|
1483
|
+
console.log();
|
|
1484
|
+
console.log(` ${c.dim}# Windows (CMD):${c.reset}`);
|
|
1485
|
+
console.log(` ${c.dim}set ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
1486
|
+
console.log(` ${c.dim}set OPENAI_API_KEY=sk-...${c.reset}`);
|
|
1487
|
+
console.log();
|
|
1488
|
+
console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
|
|
1489
|
+
console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
|
|
1490
|
+
console.log(` ${c.dim}Creates a markdown file you can paste into any LLM (Claude, ChatGPT, etc.)${c.reset}`);
|
|
1491
|
+
console.log();
|
|
1492
|
+
console.log(` ${c.bold}Option 3: Use MCP in Claude/Cursor/Windsurf (no API key needed)${c.reset}`);
|
|
1493
|
+
console.log(` ${c.dim}Add AgentAudit as MCP server — your editor's agent runs the audit using its own LLM.${c.reset}`);
|
|
1494
|
+
console.log(` ${c.dim}Config: { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }${c.reset}`);
|
|
1495
|
+
console.log();
|
|
1496
|
+
|
|
1497
|
+
// Check if --export flag
|
|
1498
|
+
if (process.argv.includes('--export')) {
|
|
1499
|
+
const exportPath = path.join(process.cwd(), `audit-${slug}.md`);
|
|
1500
|
+
const exportContent = [
|
|
1501
|
+
`# Security Audit: ${slug}`,
|
|
1502
|
+
`**Source:** ${url}`,
|
|
1503
|
+
`**Files:** ${files.length}`,
|
|
1504
|
+
``,
|
|
1505
|
+
`## Audit Instructions`,
|
|
1506
|
+
``,
|
|
1507
|
+
auditPrompt || '(audit prompt not found)',
|
|
1508
|
+
``,
|
|
1509
|
+
`## Report Format`,
|
|
1510
|
+
``,
|
|
1511
|
+
`After analysis, produce a JSON report:`,
|
|
1512
|
+
'```json',
|
|
1513
|
+
`{ "skill_slug": "${slug}", "source_url": "${url}", "risk_score": 0, "result": "safe", "findings": [] }`,
|
|
1514
|
+
'```',
|
|
1515
|
+
``,
|
|
1516
|
+
`## Source Code`,
|
|
1517
|
+
``,
|
|
1518
|
+
codeBlock,
|
|
1519
|
+
].join('\n');
|
|
1520
|
+
fs.writeFileSync(exportPath, exportContent);
|
|
1521
|
+
console.log(` ${icons.safe} Exported to ${c.bold}${exportPath}${c.reset}`);
|
|
1522
|
+
console.log(` ${c.dim}Paste this into any LLM (Claude, ChatGPT, etc.) for analysis${c.reset}`);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Cleanup
|
|
1526
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Determine actual model name for display
|
|
1531
|
+
let actualModel;
|
|
1532
|
+
if (resolvedProvider.id === 'anthropic') {
|
|
1533
|
+
actualModel = modelOverride || 'claude-sonnet-4-20250514';
|
|
1534
|
+
} else if (resolvedProvider.id === 'openrouter') {
|
|
1535
|
+
actualModel = modelOverride || process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
1536
|
+
} else if (resolvedProvider.id === 'openai') {
|
|
1537
|
+
actualModel = modelOverride || 'gpt-4o';
|
|
1538
|
+
} else if (resolvedProvider.id === 'ollama') {
|
|
1539
|
+
actualModel = modelOverride || resolvedProvider.model;
|
|
1540
|
+
} else {
|
|
1541
|
+
actualModel = modelOverride || resolvedProvider.model || 'unknown';
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// We have an API key — run LLM audit
|
|
1545
|
+
process.stdout.write(` ${c.dim}[4/4]${c.reset} Running LLM analysis ${c.dim}(${resolvedProvider.id}: ${actualModel})${c.reset}...`);
|
|
1546
|
+
|
|
1547
|
+
const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
|
|
1548
|
+
const userMessage = [
|
|
1549
|
+
`Audit this package: **${slug}** (${url})`,
|
|
1550
|
+
``,
|
|
1551
|
+
`After analysis, respond with ONLY a valid JSON object. No markdown fences, no explanation, no text before or after. Just the raw JSON:`,
|
|
1552
|
+
`{ "skill_slug": "${slug}", "source_url": "${url}", "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
|
|
1553
|
+
` "risk_score": <0-100>, "result": "<safe|caution|unsafe>", "max_severity": "<none|low|medium|high|critical>",`,
|
|
1554
|
+
` "findings_count": <n>, "findings": [{ "id": "...", "title": "...", "severity": "...", "category": "...",`,
|
|
1555
|
+
` "description": "...", "file": "...", "line": <n>, "remediation": "...", "confidence": "...", "is_by_design": false }] }`,
|
|
1556
|
+
``,
|
|
1557
|
+
`## Source Code`,
|
|
1558
|
+
codeBlock,
|
|
1559
|
+
].join('\n');
|
|
1560
|
+
|
|
1561
|
+
let report = null;
|
|
1562
|
+
let _lastLlmText = '';
|
|
1563
|
+
let providerMeta = {}; // Collect provider metadata for attestation
|
|
1564
|
+
|
|
1565
|
+
try {
|
|
1566
|
+
if (resolvedProvider.id === 'anthropic') {
|
|
1567
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
1568
|
+
method: 'POST',
|
|
1569
|
+
headers: {
|
|
1570
|
+
'x-api-key': resolvedProvider.key,
|
|
1571
|
+
'anthropic-version': '2023-06-01',
|
|
1572
|
+
'content-type': 'application/json',
|
|
1573
|
+
},
|
|
1574
|
+
body: JSON.stringify({
|
|
1575
|
+
model: modelOverride || 'claude-sonnet-4-20250514',
|
|
1576
|
+
max_tokens: 8192,
|
|
1577
|
+
system: systemPrompt,
|
|
1578
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
1579
|
+
}),
|
|
1580
|
+
signal: AbortSignal.timeout(120_000),
|
|
1581
|
+
});
|
|
1582
|
+
const data = await res.json();
|
|
1583
|
+
if (data.error) {
|
|
1584
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1585
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1586
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
const text = data.content?.[0]?.text || '';
|
|
1590
|
+
_lastLlmText = text;
|
|
1591
|
+
report = extractJSON(text);
|
|
1592
|
+
providerMeta = {
|
|
1593
|
+
provider_msg_id: data.id || null,
|
|
1594
|
+
input_tokens: data.usage?.input_tokens || null,
|
|
1595
|
+
output_tokens: data.usage?.output_tokens || null,
|
|
1596
|
+
reported_model: data.model || null,
|
|
1597
|
+
};
|
|
1598
|
+
} else {
|
|
1599
|
+
// OpenAI, OpenRouter, Ollama, or Custom (all use OpenAI-compatible chat completions API)
|
|
1600
|
+
let apiUrl, modelName, authHeaders;
|
|
1601
|
+
switch (resolvedProvider.id) {
|
|
1602
|
+
case 'openrouter':
|
|
1603
|
+
apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
|
|
1604
|
+
modelName = modelOverride || process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
1605
|
+
authHeaders = { 'Authorization': `Bearer ${resolvedProvider.key}`, 'HTTP-Referer': 'https://agentaudit.dev', 'X-Title': 'AgentAudit' };
|
|
1606
|
+
break;
|
|
1607
|
+
case 'ollama':
|
|
1608
|
+
apiUrl = `${resolvedProvider.host}/v1/chat/completions`;
|
|
1609
|
+
modelName = modelOverride || resolvedProvider.model;
|
|
1610
|
+
authHeaders = {};
|
|
1611
|
+
break;
|
|
1612
|
+
case 'custom':
|
|
1613
|
+
apiUrl = resolvedProvider.url.endsWith('/chat/completions') ? resolvedProvider.url : `${resolvedProvider.url.replace(/\/$/, '')}/chat/completions`;
|
|
1614
|
+
modelName = modelOverride || resolvedProvider.model;
|
|
1615
|
+
authHeaders = resolvedProvider.key ? { 'Authorization': `Bearer ${resolvedProvider.key}` } : {};
|
|
1616
|
+
break;
|
|
1617
|
+
default: // openai
|
|
1618
|
+
apiUrl = 'https://api.openai.com/v1/chat/completions';
|
|
1619
|
+
modelName = modelOverride || 'gpt-4o';
|
|
1620
|
+
authHeaders = { 'Authorization': `Bearer ${resolvedProvider.key}` };
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const res = await fetch(apiUrl, {
|
|
1624
|
+
method: 'POST',
|
|
1625
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
1626
|
+
body: JSON.stringify({
|
|
1627
|
+
model: modelName,
|
|
1628
|
+
max_tokens: 8192,
|
|
1629
|
+
messages: [
|
|
1630
|
+
{ role: 'system', content: systemPrompt },
|
|
1631
|
+
{ role: 'user', content: userMessage },
|
|
1632
|
+
],
|
|
1633
|
+
}),
|
|
1634
|
+
signal: AbortSignal.timeout(resolvedProvider.id === 'ollama' ? 300_000 : 120_000), // Ollama: 5min (local can be slow)
|
|
1635
|
+
});
|
|
1636
|
+
const data = await res.json();
|
|
1637
|
+
if (data.error) {
|
|
1638
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1639
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1640
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
const text = data.choices?.[0]?.message?.content || '';
|
|
1644
|
+
_lastLlmText = text;
|
|
1645
|
+
report = extractJSON(text);
|
|
1646
|
+
providerMeta = {
|
|
1647
|
+
provider_msg_id: data.id || null,
|
|
1648
|
+
provider_fingerprint: data.system_fingerprint || null,
|
|
1649
|
+
input_tokens: data.usage?.prompt_tokens || null,
|
|
1650
|
+
output_tokens: data.usage?.completion_tokens || null,
|
|
1651
|
+
reported_model: data.model || null,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
console.log(` ${c.green}done${c.reset} ${c.dim}(${elapsed(start)})${c.reset}`);
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1658
|
+
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
1659
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Cleanup repo
|
|
1664
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1665
|
+
|
|
1666
|
+
if (!report) {
|
|
1667
|
+
console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
|
|
1668
|
+
console.log(` ${c.dim}Hint: run with --debug to see the raw LLM response${c.reset}`);
|
|
1669
|
+
if (process.argv.includes('--debug')) {
|
|
1670
|
+
console.log(` ${c.dim}--- Raw LLM response (first 2000 chars) ---${c.reset}`);
|
|
1671
|
+
console.log((typeof _lastLlmText === 'string' ? _lastLlmText : '(empty)').slice(0, 2000));
|
|
1672
|
+
console.log(` ${c.dim}--- end ---${c.reset}`);
|
|
1673
|
+
}
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Display results
|
|
1678
|
+
console.log();
|
|
1679
|
+
const riskScore = report.risk_score || 0;
|
|
1680
|
+
const trustScore = 100 - riskScore;
|
|
1681
|
+
const trustColor = trustScore >= 70 ? c.green : trustScore >= 40 ? c.yellow : c.red;
|
|
1682
|
+
const trustLabel = trustScore >= 70 ? 'SAFE' : trustScore >= 40 ? 'CAUTION' : 'UNSAFE';
|
|
1683
|
+
console.log(` ${trustColor}${c.bold}${trustLabel}${c.reset} ${trustColor}Trust Score: ${trustScore}/100${c.reset} ${c.dim}(Risk: ${riskScore}/100)${c.reset}`);
|
|
1684
|
+
console.log(` ${c.dim}Model: ${resolvedProvider.id}/${actualModel} Duration: ${elapsed(start)}${c.reset}`);
|
|
1685
|
+
console.log();
|
|
1686
|
+
|
|
1687
|
+
if (report.findings && report.findings.length > 0) {
|
|
1688
|
+
console.log(` ${c.bold}Findings (${report.findings.length})${c.reset}`);
|
|
1689
|
+
console.log();
|
|
1690
|
+
for (const f of report.findings) {
|
|
1691
|
+
const sc = severityColor(f.severity);
|
|
1692
|
+
console.log(` ${severityIcon(f.severity)} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
|
|
1693
|
+
if (f.file) console.log(` ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
|
|
1694
|
+
if (f.description) console.log(` ${c.dim}${f.description.slice(0, 120)}${c.reset}`);
|
|
1695
|
+
console.log();
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
console.log(` ${c.green}No findings — package looks clean.${c.reset}`);
|
|
1699
|
+
console.log();
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Upload to registry
|
|
1703
|
+
const creds = loadCredentials();
|
|
1704
|
+
if (creds) {
|
|
1705
|
+
process.stdout.write(` Uploading report to registry...`);
|
|
1706
|
+
try {
|
|
1707
|
+
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
1708
|
+
method: 'POST',
|
|
1709
|
+
headers: {
|
|
1710
|
+
'Authorization': `Bearer ${creds.api_key}`,
|
|
1711
|
+
'Content-Type': 'application/json',
|
|
1712
|
+
},
|
|
1713
|
+
body: JSON.stringify({
|
|
1714
|
+
...report,
|
|
1715
|
+
commit_sha: report.commit_sha || repoCommitSha || undefined,
|
|
1716
|
+
package_version: report.package_version || repoPackageVersion || undefined,
|
|
1717
|
+
audit_model: providerMeta.reported_model || actualModel,
|
|
1718
|
+
audit_provider: resolvedProvider.id,
|
|
1719
|
+
provider_msg_id: providerMeta.provider_msg_id || undefined,
|
|
1720
|
+
provider_fingerprint: providerMeta.provider_fingerprint || undefined,
|
|
1721
|
+
input_tokens: providerMeta.input_tokens || undefined,
|
|
1722
|
+
output_tokens: providerMeta.output_tokens || undefined,
|
|
1723
|
+
audit_duration_ms: Date.now() - start,
|
|
1724
|
+
}),
|
|
1725
|
+
signal: AbortSignal.timeout(15_000),
|
|
1726
|
+
});
|
|
1727
|
+
if (res.ok) {
|
|
1728
|
+
const data = await res.json();
|
|
1729
|
+
const reportSlug = data?.skill_slug || data?.slug || slug;
|
|
1730
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
1731
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${reportSlug}${c.reset}`);
|
|
1732
|
+
// Refresh stats cache in background
|
|
1733
|
+
if (creds.agent_name) refreshStatsCache(creds.agent_name).catch(() => {});
|
|
1734
|
+
} else {
|
|
1735
|
+
console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
|
|
1736
|
+
}
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
console.log(` ${c.yellow}failed${c.reset}`);
|
|
1739
|
+
}
|
|
1740
|
+
} else {
|
|
1741
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to upload reports to the registry${c.reset}`);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
console.log();
|
|
1745
|
+
return report;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// ── Check command ───────────────────────────────────────
|
|
1749
|
+
|
|
1750
|
+
async function checkPackage(name, { autoAudit = false } = {}) {
|
|
1751
|
+
if (!jsonMode) {
|
|
1752
|
+
console.log(`${icons.info} Looking up ${c.bold}${name}${c.reset} in registry...`);
|
|
1753
|
+
console.log();
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const data = await checkRegistry(name);
|
|
1757
|
+
if (!data) {
|
|
1758
|
+
if (!jsonMode) {
|
|
1759
|
+
// Auto-audit: only when called from 'check' command AND input looks like a URL
|
|
1760
|
+
if (autoAudit && (name.includes('github.com') || name.includes('://'))) {
|
|
1761
|
+
console.log(` ${c.yellow}Not found in registry.${c.reset}`);
|
|
1762
|
+
console.log(` ${c.dim}Starting audit for ${name}...${c.reset}`);
|
|
1763
|
+
console.log();
|
|
1764
|
+
return await auditRepo(name);
|
|
1765
|
+
}
|
|
1766
|
+
console.log(` ${c.yellow}✖ Not found${c.reset} — "${name}" hasn't been audited yet.`);
|
|
1767
|
+
console.log();
|
|
1768
|
+
console.log(` ${c.dim}Next steps:${c.reset}`);
|
|
1769
|
+
console.log(` ${c.cyan}agentaudit check <repo-url>${c.reset} ${c.dim}Auto-lookup + audit if not found${c.reset}`);
|
|
1770
|
+
console.log(` ${c.cyan}agentaudit audit <repo-url>${c.reset} ${c.dim}Deep LLM audit${c.reset}`);
|
|
1771
|
+
console.log(` ${c.cyan}agentaudit scan <repo-url>${c.reset} ${c.dim}Quick static check (no API key)${c.reset}`);
|
|
1772
|
+
}
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
if (!jsonMode) {
|
|
1777
|
+
const riskScore = data.risk_score ?? data.latest_risk_score ?? 0;
|
|
1778
|
+
const trustScore = data.trust_score ?? (100 - riskScore);
|
|
1779
|
+
const totalFindings = data.total_findings ?? 0;
|
|
1780
|
+
const totalReports = data.total_reports ?? 0;
|
|
1781
|
+
|
|
1782
|
+
// Package name + verdict
|
|
1783
|
+
console.log(` ${c.bold}${data.display_name || name}${c.reset} ${riskBadge(riskScore)}`);
|
|
1784
|
+
if (data.description) console.log(` ${c.dim}${data.description}${c.reset}`);
|
|
1785
|
+
console.log();
|
|
1786
|
+
|
|
1787
|
+
// Trust Score (the main metric)
|
|
1788
|
+
const trustColor = trustScore >= 70 ? c.green : trustScore >= 40 ? c.yellow : c.red;
|
|
1789
|
+
const trustLabel = trustScore >= 70 ? 'SAFE' : trustScore >= 40 ? 'CAUTION' : 'UNSAFE';
|
|
1790
|
+
console.log(` ${trustColor}${c.bold}${trustLabel}${c.reset} ${trustColor}Trust Score: ${trustScore}/100${c.reset} ${c.dim}(Risk: ${riskScore}/100)${c.reset}`);
|
|
1791
|
+
|
|
1792
|
+
// Findings summary
|
|
1793
|
+
if (totalFindings > 0) {
|
|
1794
|
+
const maxSev = data.latest_max_severity;
|
|
1795
|
+
const sevStr = maxSev ? `max severity: ${severityColor(maxSev)}${maxSev}${c.reset}` : '';
|
|
1796
|
+
console.log(` ${c.dim}Findings: ${totalFindings}${sevStr ? ` (${sevStr}${c.dim})` : ''}${c.reset}`);
|
|
1797
|
+
} else {
|
|
1798
|
+
console.log(` ${c.dim}Findings: 0 (clean)${c.reset}`);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// Consensus / Confidence
|
|
1802
|
+
const uniqueAgents = data.unique_agents ?? 0;
|
|
1803
|
+
const confidence = data.confidence ?? 'unverified';
|
|
1804
|
+
const confidenceDisplay = {
|
|
1805
|
+
consensus: { icon: '🟢', label: 'Consensus Certified', color: c.green, desc: `${totalReports} reports from ${uniqueAgents} independent auditors agree` },
|
|
1806
|
+
verified: { icon: '🟢', label: 'Verified', color: c.green, desc: `${totalReports} reports from ${uniqueAgents} auditors` },
|
|
1807
|
+
low: { icon: '🟡', label: 'Low Confidence', color: c.yellow, desc: `${totalReports} reports but ${uniqueAgents <= 1 ? 'only 1 auditor' : `only ${uniqueAgents} auditors`}` },
|
|
1808
|
+
unverified: { icon: '🔴', label: 'Unverified', color: c.yellow, desc: 'Single audit, no independent confirmation' },
|
|
1809
|
+
}[confidence] || { icon: '⚪', label: confidence, color: c.dim, desc: '' };
|
|
1810
|
+
console.log(` ${confidenceDisplay.icon} ${confidenceDisplay.color}${confidenceDisplay.label}${c.reset} ${c.dim}${confidenceDisplay.desc}${c.reset}`);
|
|
1811
|
+
|
|
1812
|
+
// Audit info
|
|
1813
|
+
console.log(` ${c.dim}Reports: ${totalReports} | Auditors: ${uniqueAgents} | Last: ${data.last_audited_at ? new Date(data.last_audited_at).toLocaleDateString() : 'unknown'}${c.reset}`);
|
|
1814
|
+
if (data.has_official_audit) console.log(` ${c.green}✔ Officially audited${c.reset}`);
|
|
1815
|
+
|
|
1816
|
+
// Recommendation
|
|
1817
|
+
if (confidence === 'unverified' && trustScore >= 70) {
|
|
1818
|
+
console.log();
|
|
1819
|
+
console.log(` ${c.yellow}⚠ Score looks good but only 1 audit exists.${c.reset}`);
|
|
1820
|
+
console.log(` ${c.dim} Consider running your own audit: agentaudit audit ${data.source_url || name}${c.reset}`);
|
|
1821
|
+
} else if (confidence === 'low') {
|
|
1822
|
+
console.log();
|
|
1823
|
+
console.log(` ${c.yellow}⚠ Limited independent verification.${c.reset}`);
|
|
1824
|
+
console.log(` ${c.dim} More auditors needed for consensus. Run: agentaudit audit ${data.source_url || name}${c.reset}`);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Links
|
|
1828
|
+
console.log();
|
|
1829
|
+
if (data.source_url) console.log(` ${c.dim}Source: ${data.source_url}${c.reset}`);
|
|
1830
|
+
console.log(` ${c.dim}Registry: ${REGISTRY_URL}/skills/${encodeURIComponent(name)}${c.reset}`);
|
|
1831
|
+
console.log();
|
|
1832
|
+
}
|
|
1833
|
+
return data;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// ── Main ────────────────────────────────────────────────
|
|
1837
|
+
|
|
1838
|
+
async function main() {
|
|
1839
|
+
const rawArgs = process.argv.slice(2);
|
|
1840
|
+
|
|
1841
|
+
// MCP server mode: launched by an editor (no TTY + no args) or explicit --stdio flag
|
|
1842
|
+
if (rawArgs.includes('--stdio') || (!process.stdin.isTTY && rawArgs.length === 0)) {
|
|
1843
|
+
await import('./index.mjs');
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Parse global flags early
|
|
1848
|
+
jsonMode = rawArgs.includes('--json');
|
|
1849
|
+
quietMode = rawArgs.includes('--quiet') || rawArgs.includes('-q');
|
|
1850
|
+
// --no-color already handled at top level for `c` object
|
|
1851
|
+
|
|
1852
|
+
// --model flag: --model=<name> or --model <name>
|
|
1853
|
+
const modelFlagIdx = rawArgs.findIndex(a => a === '--model');
|
|
1854
|
+
const modelFlagEq = rawArgs.find(a => a.startsWith('--model='));
|
|
1855
|
+
modelOverride = modelFlagEq?.split('=')[1]
|
|
1856
|
+
|| (modelFlagIdx >= 0 ? rawArgs[modelFlagIdx + 1] : null)
|
|
1857
|
+
|| process.env.AGENTAUDIT_MODEL
|
|
1858
|
+
|| loadConfig()?.preferred_model
|
|
1859
|
+
|| null;
|
|
1860
|
+
globalModelOverride = modelOverride;
|
|
1861
|
+
|
|
1862
|
+
// Strip global flags from args
|
|
1863
|
+
const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color']);
|
|
1864
|
+
let args = rawArgs.filter(a => !globalFlags.has(a));
|
|
1865
|
+
// Strip --model and its value
|
|
1866
|
+
args = args.filter((a, i, arr) => {
|
|
1867
|
+
if (a.startsWith('--model=')) return false;
|
|
1868
|
+
if (a === '--model') { arr[i + 1] = '__skip__'; return false; }
|
|
1869
|
+
if (a === '__skip__') return false;
|
|
1870
|
+
return true;
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
if (args[0] === '-v' || args[0] === '--version') {
|
|
1874
|
+
console.log(`agentaudit ${getVersion()}`);
|
|
1875
|
+
process.exitCode = 0; return;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
1879
|
+
banner();
|
|
1880
|
+
console.log(` ${c.bold}USAGE${c.reset}`);
|
|
1881
|
+
console.log(` ${c.cyan}agentaudit${c.reset} <command> [options]`);
|
|
1882
|
+
console.log();
|
|
1883
|
+
console.log(` ${c.bold}SCAN & AUDIT${c.reset}`);
|
|
1884
|
+
console.log(` ${c.cyan}scan${c.reset} <url> [url...] Quick static analysis ${c.dim}(~2s, no API key)${c.reset}`);
|
|
1885
|
+
console.log(` ${c.cyan}audit${c.reset} <url> [url...] Deep LLM security audit ${c.dim}(~30s)${c.reset}`);
|
|
1886
|
+
console.log(` ${c.cyan}discover${c.reset} Find MCP servers in your editors`);
|
|
1887
|
+
console.log();
|
|
1888
|
+
console.log(` ${c.bold}REGISTRY${c.reset}`);
|
|
1889
|
+
console.log(` ${c.cyan}check${c.reset} <name|url> Look up or auto-audit package`);
|
|
1890
|
+
console.log(` ${c.cyan}lookup${c.reset} <name> Look up package in registry`);
|
|
1891
|
+
console.log();
|
|
1892
|
+
console.log(` ${c.bold}SETUP${c.reset}`);
|
|
1893
|
+
console.log(` ${c.cyan}status${c.reset} Check providers & API keys`);
|
|
1894
|
+
console.log(` ${c.cyan}setup${c.reset} Register & configure`);
|
|
1895
|
+
console.log(` ${c.cyan}models${c.reset} List available LLM models`);
|
|
1896
|
+
console.log(` ${c.cyan}config set${c.reset} <key> <value> Set default provider/options`);
|
|
1897
|
+
console.log();
|
|
1898
|
+
console.log(` ${c.bold}OPTIONS${c.reset}`);
|
|
1899
|
+
console.log(` ${c.dim}--json Machine-readable JSON output${c.reset}`);
|
|
1900
|
+
console.log(` ${c.dim}--quiet Suppress banner${c.reset}`);
|
|
1901
|
+
console.log(` ${c.dim}--no-color Disable colors ${c.reset}${c.dim}(also: NO_COLOR=1)${c.reset}`);
|
|
1902
|
+
console.log(` ${c.dim}--provider <p> Force provider ${c.reset}${c.dim}(anthropic|openai|openrouter|ollama|custom)${c.reset}`);
|
|
1903
|
+
console.log(` ${c.dim}--model <m> Override model ${c.reset}${c.dim}(e.g. gpt-4o-mini, claude-3.5-sonnet)${c.reset}`);
|
|
1904
|
+
console.log(` ${c.dim}--export Export audit payload to markdown${c.reset}`);
|
|
1905
|
+
console.log(` ${c.dim}--debug Show raw LLM response on errors${c.reset}`);
|
|
1906
|
+
console.log();
|
|
1907
|
+
console.log(` ${c.bold}EXAMPLES${c.reset}`);
|
|
1908
|
+
console.log(` ${c.dim}$${c.reset} agentaudit scan https://github.com/owner/repo`);
|
|
1909
|
+
console.log(` ${c.dim}$${c.reset} agentaudit audit https://github.com/owner/repo`);
|
|
1910
|
+
console.log(` ${c.dim}$${c.reset} agentaudit check fastmcp`);
|
|
1911
|
+
console.log(` ${c.dim}$${c.reset} agentaudit status`);
|
|
1912
|
+
console.log();
|
|
1913
|
+
console.log(` ${c.bold}PROVIDERS${c.reset} ${c.dim}(set any one for deep audits)${c.reset}`);
|
|
1914
|
+
console.log(` ${c.dim}ANTHROPIC_API_KEY · OPENAI_API_KEY · OPENROUTER_API_KEY · OLLAMA_MODEL · LLM_API_URL${c.reset}`);
|
|
1915
|
+
console.log(` ${c.dim}Set default: AGENTAUDIT_PROVIDER=openai AGENTAUDIT_MODEL=gpt-4o-mini${c.reset}`);
|
|
1916
|
+
console.log(` ${c.dim}Or persist: agentaudit config set provider openai${c.reset}`);
|
|
1917
|
+
console.log(` ${c.dim} agentaudit config set model gpt-4o-mini${c.reset}`);
|
|
1918
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit status${c.dim} to check configuration.${c.reset}`);
|
|
1919
|
+
console.log();
|
|
1920
|
+
process.exitCode = 0; return;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Default no-arg → discover
|
|
1924
|
+
const command = args.length === 0 ? 'discover' : args[0];
|
|
1925
|
+
const targets = args.slice(1);
|
|
1926
|
+
|
|
1927
|
+
banner();
|
|
1928
|
+
|
|
1929
|
+
if (command === 'setup') {
|
|
1930
|
+
await setupCommand();
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
if (command === 'status') {
|
|
1935
|
+
console.log(` ${c.bold}LLM Providers:${c.reset}`);
|
|
1936
|
+
console.log();
|
|
1937
|
+
const keys = {
|
|
1938
|
+
anthropicKey: process.env.ANTHROPIC_API_KEY,
|
|
1939
|
+
openaiKey: process.env.OPENAI_API_KEY,
|
|
1940
|
+
openrouterKey: process.env.OPENROUTER_API_KEY,
|
|
1941
|
+
};
|
|
1942
|
+
const ollamaHost = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
1943
|
+
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
1944
|
+
const customUrl = process.env.LLM_API_URL;
|
|
1945
|
+
|
|
1946
|
+
const checks = [
|
|
1947
|
+
{ name: 'Anthropic', env: 'ANTHROPIC_API_KEY', key: keys.anthropicKey, testUrl: 'https://api.anthropic.com/v1/messages', testHeaders: (k) => ({ 'x-api-key': k, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' }), testBody: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) },
|
|
1948
|
+
{ name: 'OpenAI', env: 'OPENAI_API_KEY', key: keys.openaiKey, testUrl: 'https://api.openai.com/v1/chat/completions', testHeaders: (k) => ({ 'Authorization': `Bearer ${k}`, 'Content-Type': 'application/json' }), testBody: JSON.stringify({ model: 'gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) },
|
|
1949
|
+
{ name: 'OpenRouter', env: 'OPENROUTER_API_KEY', key: keys.openrouterKey, testUrl: 'https://openrouter.ai/api/v1/chat/completions', testHeaders: (k) => ({ 'Authorization': `Bearer ${k}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://agentaudit.dev', 'X-Title': 'AgentAudit' }), testBody: JSON.stringify({ model: 'openai/gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) },
|
|
1950
|
+
{ name: 'Ollama', env: 'OLLAMA_MODEL', key: ollamaModel, testUrl: `${ollamaHost}/api/tags`, testHeaders: () => ({}), testBody: null },
|
|
1951
|
+
{ name: 'Custom', env: 'LLM_API_URL', key: customUrl, testUrl: customUrl ? `${customUrl.replace(/\/$/, '')}/models` : null, testHeaders: (k) => process.env.LLM_API_KEY ? ({ 'Authorization': `Bearer ${process.env.LLM_API_KEY}` }) : {}, testBody: null },
|
|
1952
|
+
];
|
|
1953
|
+
|
|
1954
|
+
for (const p of checks) {
|
|
1955
|
+
if (!p.key) {
|
|
1956
|
+
console.log(` ${c.dim}○${c.reset} ${p.name.padEnd(12)} ${c.dim}not set${c.reset} ${c.dim}(${p.env})${c.reset}`);
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
const masked = p.key.substring(0, 8) + '...' + p.key.substring(p.key.length - 4);
|
|
1960
|
+
process.stdout.write(` ${c.yellow}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} checking...`);
|
|
1961
|
+
try {
|
|
1962
|
+
const res = await fetch(p.testUrl, {
|
|
1963
|
+
method: p.testBody ? 'POST' : 'GET',
|
|
1964
|
+
headers: p.testHeaders(p.key),
|
|
1965
|
+
...(p.testBody ? { body: p.testBody } : {}),
|
|
1966
|
+
signal: AbortSignal.timeout(10_000),
|
|
1967
|
+
});
|
|
1968
|
+
if (res.ok || res.status === 200 || res.status === 201) {
|
|
1969
|
+
process.stdout.write(`\r ${c.green}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.green}valid ✓${c.reset} \n`);
|
|
1970
|
+
} else {
|
|
1971
|
+
const body = await res.json().catch(() => ({}));
|
|
1972
|
+
const rawMsg = body?.error?.message || body?.message || `HTTP ${res.status}`;
|
|
1973
|
+
// Detect specific error types for clearer messages
|
|
1974
|
+
const lcMsg = rawMsg.toLowerCase();
|
|
1975
|
+
let errMsg = rawMsg;
|
|
1976
|
+
let hint = '';
|
|
1977
|
+
if (lcMsg.includes('credit') || lcMsg.includes('balance') || lcMsg.includes('quota') || lcMsg.includes('billing') || lcMsg.includes('exceeded') || lcMsg.includes('insufficient')) {
|
|
1978
|
+
errMsg = 'no credits';
|
|
1979
|
+
if (p.name === 'Anthropic') hint = `\n ${c.dim}└─ Add credits: console.anthropic.com/settings/plans${c.reset}`;
|
|
1980
|
+
else if (p.name === 'OpenAI') hint = `\n ${c.dim}└─ Check usage: platform.openai.com/usage${c.reset}`;
|
|
1981
|
+
else if (p.name === 'OpenRouter') hint = `\n ${c.dim}└─ Check balance: openrouter.ai/credits${c.reset}`;
|
|
1982
|
+
} else if (res.status === 401 || lcMsg.includes('invalid') || lcMsg.includes('unauthorized') || lcMsg.includes('authentication')) {
|
|
1983
|
+
errMsg = 'invalid key';
|
|
1984
|
+
if (p.name === 'Anthropic') hint = `\n ${c.dim}└─ Check key: console.anthropic.com/settings/keys${c.reset}`;
|
|
1985
|
+
else if (p.name === 'OpenAI') hint = `\n ${c.dim}└─ Check key: platform.openai.com/api-keys${c.reset}`;
|
|
1986
|
+
} else if (res.status === 429) {
|
|
1987
|
+
errMsg = 'rate limited';
|
|
1988
|
+
hint = `\n ${c.dim}└─ Try again in a moment${c.reset}`;
|
|
1989
|
+
}
|
|
1990
|
+
process.stdout.write(`\r ${c.red}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.red}✖ ${errMsg}${c.reset}${hint} \n`);
|
|
1991
|
+
}
|
|
1992
|
+
} catch (e) {
|
|
1993
|
+
process.stdout.write(`\r ${c.red}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.red}error ✗${c.reset} ${c.dim}(${e.message})${c.reset} \n`);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const resolved = resolveProvider(null, keys);
|
|
1998
|
+
console.log();
|
|
1999
|
+
if (resolved) {
|
|
2000
|
+
const activeModel = modelOverride || process.env.AGENTAUDIT_MODEL || loadConfig()?.preferred_model;
|
|
2001
|
+
console.log(` ${c.bold}Active:${c.reset} ${c.green}${resolved.label}${c.reset}${activeModel ? ` ${c.dim}model: ${activeModel}${c.reset}` : ''}`);
|
|
2002
|
+
console.log(` ${c.dim}Override: --provider=<name> --model=<name>${c.reset}`);
|
|
2003
|
+
console.log(` ${c.dim}Set default: agentaudit config set provider <name>${c.reset}`);
|
|
2004
|
+
console.log(` ${c.dim} agentaudit config set model <name>${c.reset}`);
|
|
2005
|
+
} else {
|
|
2006
|
+
console.log(` ${c.yellow}⚠ No working LLM provider.${c.reset} Deep audits require one.`);
|
|
2007
|
+
console.log(` ${c.dim}Set a key: export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
2008
|
+
console.log(` ${c.dim}Or scan without LLM: agentaudit scan <url>${c.reset}`);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// AgentAudit registry key
|
|
2012
|
+
console.log();
|
|
2013
|
+
console.log(` ${c.bold}Registry:${c.reset}`);
|
|
2014
|
+
const creds = loadCredentials();
|
|
2015
|
+
if (creds?.api_key) {
|
|
2016
|
+
const masked = creds.api_key.substring(0, 8) + '...' + creds.api_key.substring(creds.api_key.length - 4);
|
|
2017
|
+
console.log(` ${c.green}●${c.reset} AgentAudit ${c.dim}${masked}${c.reset} ${c.dim}(${creds.agent_name || 'unknown'})${c.reset}`);
|
|
2018
|
+
// Fetch agent stats from leaderboard
|
|
2019
|
+
try {
|
|
2020
|
+
const lbRes = await fetch(`${REGISTRY_URL}/api/leaderboard`, { signal: AbortSignal.timeout(5000) });
|
|
2021
|
+
if (lbRes.ok) {
|
|
2022
|
+
const agents = await lbRes.json();
|
|
2023
|
+
const myName = creds.agent_name?.toLowerCase();
|
|
2024
|
+
const idx = Array.isArray(agents) ? agents.findIndex((a) => (a.agent_name || '').toLowerCase() === myName) : -1;
|
|
2025
|
+
if (idx >= 0) {
|
|
2026
|
+
const me = agents[idx];
|
|
2027
|
+
const pts = me.total_points || 0;
|
|
2028
|
+
const reports = me.total_reports || 0;
|
|
2029
|
+
const rank = idx + 1;
|
|
2030
|
+
const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ' ';
|
|
2031
|
+
console.log();
|
|
2032
|
+
console.log(` ${c.bold}Your Stats:${c.reset}`);
|
|
2033
|
+
console.log(` ${medal} Rank #${rank} of ${agents.length} ${c.dim}│${c.reset} ${c.cyan}${pts}${c.reset} points ${c.dim}│${c.reset} ${reports} reports`);
|
|
2034
|
+
if (me.is_official) console.log(` ${c.green}✔ Official Auditor${c.reset}`);
|
|
2035
|
+
// Update stats cache
|
|
2036
|
+
saveStatsCache({ rank, total: agents.length, pts, reports, official: !!me.is_official });
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
} catch {}
|
|
2040
|
+
} else {
|
|
2041
|
+
console.log(` ${c.dim}○${c.reset} AgentAudit ${c.dim}not set${c.reset} ${c.dim}(run: agentaudit setup)${c.reset}`);
|
|
2042
|
+
}
|
|
2043
|
+
console.log();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (command === 'discover') {
|
|
2048
|
+
const scanFlag = targets.includes('--quick') || targets.includes('--scan') || targets.includes('-s');
|
|
2049
|
+
const auditFlag = targets.includes('--deep') || targets.includes('--audit') || targets.includes('-a');
|
|
2050
|
+
await discoverCommand({ scan: scanFlag, audit: auditFlag });
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
if (command === 'models') {
|
|
2055
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
2056
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
2057
|
+
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
|
2058
|
+
|
|
2059
|
+
console.log(` ${c.bold}Available models by provider:${c.reset}`);
|
|
2060
|
+
console.log();
|
|
2061
|
+
|
|
2062
|
+
// Static lists for Anthropic (no list API)
|
|
2063
|
+
console.log(` ${c.bold}Anthropic${c.reset}${anthropicKey ? ` ${c.green}(configured)${c.reset}` : ` ${c.dim}(not configured)${c.reset}`}`);
|
|
2064
|
+
console.log(` ${c.dim}claude-sonnet-4-20250514${c.reset} ${c.dim}(default)${c.reset}`);
|
|
2065
|
+
console.log(` ${c.dim}claude-opus-4-20250514${c.reset}`);
|
|
2066
|
+
console.log(` ${c.dim}claude-haiku-3-20250514${c.reset}`);
|
|
2067
|
+
console.log();
|
|
2068
|
+
|
|
2069
|
+
// Static list for OpenAI
|
|
2070
|
+
console.log(` ${c.bold}OpenAI${c.reset}${openaiKey ? ` ${c.green}(configured)${c.reset}` : ` ${c.dim}(not configured)${c.reset}`}`);
|
|
2071
|
+
console.log(` ${c.dim}gpt-4o${c.reset} ${c.dim}(default)${c.reset}`);
|
|
2072
|
+
console.log(` ${c.dim}gpt-4o-mini${c.reset}`);
|
|
2073
|
+
console.log(` ${c.dim}gpt-4.1${c.reset}`);
|
|
2074
|
+
console.log(` ${c.dim}gpt-4.1-mini${c.reset}`);
|
|
2075
|
+
console.log(` ${c.dim}o3${c.reset}`);
|
|
2076
|
+
console.log(` ${c.dim}o4-mini${c.reset}`);
|
|
2077
|
+
console.log();
|
|
2078
|
+
|
|
2079
|
+
// OpenRouter — fetch from API
|
|
2080
|
+
console.log(` ${c.bold}OpenRouter${c.reset}${openrouterKey ? ` ${c.green}(configured)${c.reset}` : ` ${c.dim}(not configured)${c.reset}`}`);
|
|
2081
|
+
if (openrouterKey || targets.includes('--all')) {
|
|
2082
|
+
process.stdout.write(` ${c.dim}Fetching models...${c.reset}`);
|
|
2083
|
+
try {
|
|
2084
|
+
const res = await fetch('https://openrouter.ai/api/v1/models', {
|
|
2085
|
+
headers: openrouterKey ? { 'Authorization': `Bearer ${openrouterKey}` } : {},
|
|
2086
|
+
signal: AbortSignal.timeout(10_000),
|
|
2087
|
+
});
|
|
2088
|
+
const data = await res.json();
|
|
2089
|
+
const models = (data.data || [])
|
|
2090
|
+
.filter(m => m.id && !m.id.includes(':free') && !m.id.includes('/extended'))
|
|
2091
|
+
.sort((a, b) => (a.id || '').localeCompare(b.id || ''));
|
|
2092
|
+
|
|
2093
|
+
// Group by provider prefix
|
|
2094
|
+
const groups = {};
|
|
2095
|
+
for (const m of models) {
|
|
2096
|
+
const [prefix] = m.id.split('/');
|
|
2097
|
+
if (!groups[prefix]) groups[prefix] = [];
|
|
2098
|
+
groups[prefix].push(m);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// Show popular ones first
|
|
2102
|
+
const popular = ['anthropic', 'openai', 'google', 'meta-llama', 'mistralai', 'deepseek'];
|
|
2103
|
+
const shown = new Set();
|
|
2104
|
+
process.stdout.write(`\r ${c.green}${models.length} models available${c.reset} \n`);
|
|
2105
|
+
console.log();
|
|
2106
|
+
|
|
2107
|
+
for (const prefix of popular) {
|
|
2108
|
+
if (!groups[prefix]) continue;
|
|
2109
|
+
shown.add(prefix);
|
|
2110
|
+
console.log(` ${c.bold}${prefix}${c.reset}`);
|
|
2111
|
+
for (const m of groups[prefix].slice(0, 5)) {
|
|
2112
|
+
console.log(` ${c.dim}${m.id}${c.reset}`);
|
|
2113
|
+
}
|
|
2114
|
+
if (groups[prefix].length > 5) {
|
|
2115
|
+
console.log(` ${c.dim}... and ${groups[prefix].length - 5} more${c.reset}`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
const otherCount = Object.keys(groups).filter(k => !shown.has(k)).length;
|
|
2120
|
+
if (otherCount > 0) {
|
|
2121
|
+
console.log();
|
|
2122
|
+
console.log(` ${c.dim}+ ${otherCount} more providers. Use --model=<provider/model>${c.reset}`);
|
|
2123
|
+
console.log(` ${c.dim}Full list: https://openrouter.ai/models${c.reset}`);
|
|
2124
|
+
}
|
|
2125
|
+
} catch (e) {
|
|
2126
|
+
process.stdout.write(`\r ${c.red}Failed to fetch: ${e.message}${c.reset} \n`);
|
|
2127
|
+
}
|
|
2128
|
+
} else {
|
|
2129
|
+
console.log(` ${c.dim}anthropic/claude-sonnet-4${c.reset} ${c.dim}(default)${c.reset}`);
|
|
2130
|
+
console.log(` ${c.dim}Set OPENROUTER_API_KEY to see all ${c.bold}200+${c.reset}${c.dim} models${c.reset}`);
|
|
2131
|
+
console.log(` ${c.dim}Or browse: https://openrouter.ai/models${c.reset}`);
|
|
2132
|
+
}
|
|
2133
|
+
console.log();
|
|
2134
|
+
|
|
2135
|
+
// Ollama
|
|
2136
|
+
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
2137
|
+
const ollamaHost = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
2138
|
+
console.log(` ${c.bold}Ollama${c.reset}${ollamaModel ? ` ${c.green}(configured: ${ollamaModel})${c.reset}` : ` ${c.dim}(not configured)${c.reset}`}`);
|
|
2139
|
+
if (ollamaModel || process.env.OLLAMA_HOST) {
|
|
2140
|
+
try {
|
|
2141
|
+
const res = await fetch(`${ollamaHost}/api/tags`, { signal: AbortSignal.timeout(5_000) });
|
|
2142
|
+
const data = await res.json();
|
|
2143
|
+
for (const m of (data.models || []).slice(0, 10)) {
|
|
2144
|
+
console.log(` ${c.dim}${m.name}${c.reset}`);
|
|
2145
|
+
}
|
|
2146
|
+
} catch {
|
|
2147
|
+
console.log(` ${c.dim}(Ollama not running at ${ollamaHost})${c.reset}`);
|
|
2148
|
+
}
|
|
2149
|
+
} else {
|
|
2150
|
+
console.log(` ${c.dim}Set OLLAMA_MODEL to use local models${c.reset}`);
|
|
2151
|
+
}
|
|
2152
|
+
console.log();
|
|
2153
|
+
|
|
2154
|
+
console.log(` ${c.bold}Set model:${c.reset}`);
|
|
2155
|
+
console.log(` ${c.cyan}agentaudit config set model <name>${c.reset}`);
|
|
2156
|
+
console.log(` ${c.cyan}agentaudit audit <url> --model <name>${c.reset}`);
|
|
2157
|
+
console.log(` ${c.dim}Or env: AGENTAUDIT_MODEL=<name>${c.reset}`);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
if (command === 'config') {
|
|
2162
|
+
const subCmd = targets[0];
|
|
2163
|
+
if (subCmd === 'set' && targets[1] === 'provider' && targets[2]) {
|
|
2164
|
+
const validProviders = ['anthropic', 'openai', 'openrouter', 'ollama', 'custom', 'claude', 'gpt'];
|
|
2165
|
+
const val = targets[2].toLowerCase();
|
|
2166
|
+
if (!validProviders.includes(val)) {
|
|
2167
|
+
console.log(` ${c.red}✖ Unknown provider: ${val}${c.reset}`);
|
|
2168
|
+
console.log(` ${c.dim}Valid: anthropic, openai, openrouter, ollama, custom${c.reset}`);
|
|
2169
|
+
process.exitCode = 2; return;
|
|
2170
|
+
}
|
|
2171
|
+
saveConfig({ preferred_provider: val });
|
|
2172
|
+
console.log(` ${c.green}✔${c.reset} Default provider set to: ${c.bold}${val}${c.reset}`);
|
|
2173
|
+
console.log(` ${c.dim}Override per-command: --provider=<name>${c.reset}`);
|
|
2174
|
+
console.log(` ${c.dim}Or env: AGENTAUDIT_PROVIDER=<name>${c.reset}`);
|
|
2175
|
+
} else if (subCmd === 'set' && targets[1] === 'model' && targets[2]) {
|
|
2176
|
+
const val = targets[2];
|
|
2177
|
+
saveConfig({ preferred_model: val });
|
|
2178
|
+
console.log(` ${c.green}✔${c.reset} Default model set to: ${c.bold}${val}${c.reset}`);
|
|
2179
|
+
console.log(` ${c.dim}Override per-command: --model=<name>${c.reset}`);
|
|
2180
|
+
console.log(` ${c.dim}Or env: AGENTAUDIT_MODEL=<name>${c.reset}`);
|
|
2181
|
+
} else if (subCmd === 'get' || !subCmd) {
|
|
2182
|
+
const cfg = loadConfig();
|
|
2183
|
+
console.log(` ${c.bold}Config:${c.reset} ${USER_CONFIG_FILE}`);
|
|
2184
|
+
if (Object.keys(cfg).length === 0) {
|
|
2185
|
+
console.log(` ${c.dim}(empty — using defaults)${c.reset}`);
|
|
2186
|
+
} else {
|
|
2187
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
2188
|
+
console.log(` ${c.dim}${k}:${c.reset} ${v}`);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
} else if (subCmd === 'reset') {
|
|
2192
|
+
try { fs.unlinkSync(USER_CONFIG_FILE); } catch {}
|
|
2193
|
+
console.log(` ${c.green}✔${c.reset} Config reset to defaults.`);
|
|
2194
|
+
} else {
|
|
2195
|
+
console.log(` ${c.red}✖ Unknown config command${c.reset}`);
|
|
2196
|
+
console.log(` ${c.dim}Usage: agentaudit config set provider <name>${c.reset}`);
|
|
2197
|
+
console.log(` ${c.dim} agentaudit config get${c.reset}`);
|
|
2198
|
+
console.log(` ${c.dim} agentaudit config reset${c.reset}`);
|
|
2199
|
+
}
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
if (command === 'lookup' || command === 'check') {
|
|
2204
|
+
const names = targets.filter(t => !t.startsWith('--'));
|
|
2205
|
+
if (names.length === 0) {
|
|
2206
|
+
console.log(` ${c.red}✖ Package name or URL required${c.reset}`);
|
|
2207
|
+
console.log(` ${c.dim}Usage: agentaudit check <name|url>${c.reset}`);
|
|
2208
|
+
process.exitCode = 2;
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const results = [];
|
|
2212
|
+
const allowAutoAudit = command === 'check'; // only 'check' auto-audits, 'lookup' never does
|
|
2213
|
+
for (const t of names) {
|
|
2214
|
+
const data = await checkPackage(t, { autoAudit: allowAutoAudit });
|
|
2215
|
+
results.push(data);
|
|
2216
|
+
}
|
|
2217
|
+
if (jsonMode) {
|
|
2218
|
+
console.log(JSON.stringify(results.length === 1 ? (results[0] || { error: 'not_found' }) : results, null, 2));
|
|
2219
|
+
}
|
|
2220
|
+
process.exitCode = 0; return;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
if (command === 'scan') {
|
|
2224
|
+
const urls = targets.filter(t => !t.startsWith('--'));
|
|
2225
|
+
if (urls.length === 0) {
|
|
2226
|
+
console.log(` ${c.red}✖ Repository URL required${c.reset}`);
|
|
2227
|
+
console.log(` ${c.dim}Usage: agentaudit scan <url>${c.reset}`);
|
|
2228
|
+
console.log(` ${c.dim}Or discover local servers: ${c.cyan}agentaudit discover${c.reset}`);
|
|
2229
|
+
process.exitCode = 2;
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
const results = [];
|
|
2234
|
+
let hadErrors = false;
|
|
2235
|
+
for (const url of urls) {
|
|
2236
|
+
const result = await scanRepo(url);
|
|
2237
|
+
if (result) results.push(result);
|
|
2238
|
+
else hadErrors = true;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (jsonMode) {
|
|
2242
|
+
const jsonOut = results.map(r => ({
|
|
2243
|
+
slug: r.slug,
|
|
2244
|
+
url: r.url,
|
|
2245
|
+
findings: r.findings.map(f => ({
|
|
2246
|
+
severity: f.severity,
|
|
2247
|
+
title: f.title,
|
|
2248
|
+
file: f.file,
|
|
2249
|
+
line: f.line,
|
|
2250
|
+
snippet: f.snippet,
|
|
2251
|
+
})),
|
|
2252
|
+
fileCount: r.files,
|
|
2253
|
+
duration: r.duration,
|
|
2254
|
+
}));
|
|
2255
|
+
console.log(JSON.stringify(jsonOut.length === 1 ? jsonOut[0] : jsonOut, null, 2));
|
|
2256
|
+
} else if (results.length > 1) {
|
|
2257
|
+
printSummary(results);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
if (hadErrors && results.length === 0) { process.exitCode = 2; return; }
|
|
2261
|
+
const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2262
|
+
process.exitCode = totalFindings > 0 ? 1 : 0;
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (command === 'audit') {
|
|
2267
|
+
const urls = targets.filter(t => !t.startsWith('--'));
|
|
2268
|
+
if (urls.length === 0) {
|
|
2269
|
+
console.log(` ${c.red}✖ Repository URL required${c.reset}`);
|
|
2270
|
+
console.log(` ${c.dim}Usage: agentaudit audit <url>${c.reset}`);
|
|
2271
|
+
process.exitCode = 2;
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
let hasFindings = false;
|
|
2276
|
+
for (const url of urls) {
|
|
2277
|
+
const report = await auditRepo(url);
|
|
2278
|
+
if (report?.findings?.length > 0) hasFindings = true;
|
|
2279
|
+
}
|
|
2280
|
+
process.exitCode = hasFindings ? 1 : 0;
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// Typo correction via Levenshtein distance
|
|
2285
|
+
const knownCommands = ['discover', 'scan', 'audit', 'check', 'lookup', 'status', 'setup', 'config', 'models'];
|
|
2286
|
+
const suggestion = knownCommands
|
|
2287
|
+
.map(cmd => ({ cmd, dist: levenshtein(command, cmd) }))
|
|
2288
|
+
.filter(x => x.dist <= 3)
|
|
2289
|
+
.sort((a, b) => a.dist - b.dist)[0];
|
|
2290
|
+
|
|
2291
|
+
console.log(` ${c.red}✖ Unknown command: ${command}${c.reset}`);
|
|
2292
|
+
if (suggestion) {
|
|
2293
|
+
console.log(` ${c.dim}Did you mean: ${c.cyan}agentaudit ${suggestion.cmd}${c.reset}${c.dim}?${c.reset}`);
|
|
2294
|
+
}
|
|
2295
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit --help${c.dim} for usage${c.reset}`);
|
|
2296
|
+
process.exitCode = 2;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
main().catch(err => {
|
|
2300
|
+
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
2301
|
+
process.exitCode = 2;
|
|
2302
|
+
});
|