agentaudit 3.9.48 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +591 -1110
- package/index.mjs +615 -687
- package/package.json +47 -45
- package/postinstall.mjs +18 -0
package/cli.mjs
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* AgentAudit CLI — Security scanner for AI
|
|
3
|
+
* AgentAudit CLI — Security scanner for AI packages
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* agentaudit Discover local MCP servers
|
|
7
|
+
* agentaudit discover [--quick|--deep] Find MCP servers in AI editors
|
|
8
|
+
* agentaudit scan <repo-url> [--deep] Quick scan (or deep audit with --deep)
|
|
9
|
+
* agentaudit audit <repo-url> Deep LLM-powered security audit
|
|
10
|
+
* agentaudit lookup <name> Look up package in registry
|
|
11
|
+
* agentaudit setup Register + configure API key
|
|
8
12
|
*
|
|
9
|
-
* Global flags: --json, --quiet, --no-color
|
|
13
|
+
* Global flags: --json, --quiet, --no-color
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import fs from 'fs';
|
|
13
17
|
import os from 'os';
|
|
14
18
|
import path from 'path';
|
|
15
|
-
import { execSync } from 'child_process';
|
|
19
|
+
import { execSync, execFileSync } from 'child_process';
|
|
16
20
|
import { createInterface } from 'readline';
|
|
17
21
|
import { fileURLToPath } from 'url';
|
|
18
22
|
|
|
@@ -20,62 +24,30 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
20
24
|
const SKILL_DIR = path.resolve(__dirname);
|
|
21
25
|
const REGISTRY_URL = 'https://agentaudit.dev';
|
|
22
26
|
|
|
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 p;
|
|
52
|
-
// Preferred provider not available (no API key) — fall through to inference
|
|
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
27
|
// ── Global flags (set in main before command routing) ────
|
|
74
28
|
let jsonMode = false;
|
|
75
29
|
let quietMode = false;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
30
|
+
|
|
31
|
+
// ── LLM Provider Registry ───────────────────────────────
|
|
32
|
+
const LLM_PROVIDERS = [
|
|
33
|
+
// Native APIs (unique formats)
|
|
34
|
+
{ key: 'ANTHROPIC_API_KEY', name: 'Anthropic (Claude)', provider: 'anthropic', type: 'anthropic', model: 'claude-sonnet-4-20250514', url: 'https://api.anthropic.com/v1/messages' },
|
|
35
|
+
{ key: 'GEMINI_API_KEY', name: 'Google (Gemini)', provider: 'google', type: 'gemini', model: 'gemini-2.5-flash', url: 'https://generativelanguage.googleapis.com/v1beta/models' },
|
|
36
|
+
{ key: 'GOOGLE_API_KEY', name: 'Google (Gemini)', provider: 'google', type: 'gemini', model: 'gemini-2.5-flash', url: 'https://generativelanguage.googleapis.com/v1beta/models' },
|
|
37
|
+
// OpenAI-compatible APIs
|
|
38
|
+
{ key: 'OPENAI_API_KEY', name: 'OpenAI (GPT-4o)', provider: 'openai', type: 'openai', model: 'gpt-4o', url: 'https://api.openai.com/v1/chat/completions' },
|
|
39
|
+
{ key: 'DEEPSEEK_API_KEY', name: 'DeepSeek', provider: 'deepseek', type: 'openai', model: 'deepseek-chat', url: 'https://api.deepseek.com/v1/chat/completions' },
|
|
40
|
+
{ key: 'MISTRAL_API_KEY', name: 'Mistral', provider: 'mistral', type: 'openai', model: 'mistral-large-latest', url: 'https://api.mistral.ai/v1/chat/completions' },
|
|
41
|
+
{ key: 'GROQ_API_KEY', name: 'Groq', provider: 'groq', type: 'openai', model: 'llama-3.3-70b-versatile', url: 'https://api.groq.com/openai/v1/chat/completions' },
|
|
42
|
+
{ key: 'XAI_API_KEY', name: 'xAI (Grok)', provider: 'xai', type: 'openai', model: 'grok-3', url: 'https://api.x.ai/v1/chat/completions' },
|
|
43
|
+
{ key: 'TOGETHER_API_KEY', name: 'Together AI', provider: 'together', type: 'openai', model: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', url: 'https://api.together.xyz/v1/chat/completions' },
|
|
44
|
+
{ key: 'FIREWORKS_API_KEY', name: 'Fireworks AI', provider: 'fireworks', type: 'openai', model: 'accounts/fireworks/models/llama-v3p3-70b-instruct', url: 'https://api.fireworks.ai/inference/v1/chat/completions' },
|
|
45
|
+
{ key: 'CEREBRAS_API_KEY', name: 'Cerebras', provider: 'cerebras', type: 'openai', model: 'llama-3.3-70b', url: 'https://api.cerebras.ai/v1/chat/completions' },
|
|
46
|
+
{ key: 'ZAI_API_KEY', name: 'Zhipu AI (GLM)', provider: 'zhipu', type: 'openai', model: 'glm-4.7', url: 'https://api.z.ai/api/paas/v4/chat/completions' },
|
|
47
|
+
{ key: 'ZHIPUAI_API_KEY', name: 'Zhipu AI (GLM)', provider: 'zhipu', type: 'openai', model: 'glm-4.7', url: 'https://api.z.ai/api/paas/v4/chat/completions' },
|
|
48
|
+
// Meta-provider (routes to any model)
|
|
49
|
+
{ key: 'OPENROUTER_API_KEY', name: 'OpenRouter', provider: 'openrouter', type: 'openai', model: 'anthropic/claude-sonnet-4', url: 'https://openrouter.ai/api/v1/chat/completions' },
|
|
50
|
+
];
|
|
79
51
|
|
|
80
52
|
// ── ANSI Colors (respects NO_COLOR and --no-color) ───────
|
|
81
53
|
|
|
@@ -137,55 +109,47 @@ function loadCredentials() {
|
|
|
137
109
|
return null;
|
|
138
110
|
}
|
|
139
111
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
112
|
+
function loadLlmConfig() {
|
|
113
|
+
for (const f of [SKILL_CRED_FILE, USER_CRED_FILE]) {
|
|
114
|
+
if (fs.existsSync(f)) {
|
|
115
|
+
try {
|
|
116
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
117
|
+
if (data.llm_model) return { llm_model: data.llm_model };
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function saveLlmConfig(model) {
|
|
125
|
+
// Merge into existing credentials
|
|
126
|
+
let existing = {};
|
|
127
|
+
if (fs.existsSync(USER_CRED_FILE)) {
|
|
128
|
+
try { existing = JSON.parse(fs.readFileSync(USER_CRED_FILE, 'utf8')); } catch {}
|
|
129
|
+
}
|
|
130
|
+
existing.llm_model = model;
|
|
131
|
+
const json = JSON.stringify(existing, null, 2);
|
|
142
132
|
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
143
133
|
fs.writeFileSync(USER_CRED_FILE, json, { mode: 0o600 });
|
|
144
134
|
try {
|
|
135
|
+
let skillExisting = {};
|
|
136
|
+
if (fs.existsSync(SKILL_CRED_FILE)) {
|
|
137
|
+
try { skillExisting = JSON.parse(fs.readFileSync(SKILL_CRED_FILE, 'utf8')); } catch {}
|
|
138
|
+
}
|
|
139
|
+
skillExisting.llm_model = model;
|
|
145
140
|
fs.mkdirSync(path.dirname(SKILL_CRED_FILE), { recursive: true });
|
|
146
|
-
fs.writeFileSync(SKILL_CRED_FILE,
|
|
141
|
+
fs.writeFileSync(SKILL_CRED_FILE, JSON.stringify(skillExisting, null, 2), { mode: 0o600 });
|
|
147
142
|
} catch {}
|
|
148
143
|
}
|
|
149
144
|
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
try { return fs.existsSync(USER_STATS_FILE) ? JSON.parse(fs.readFileSync(USER_STATS_FILE, 'utf8')) : null; } catch { return null; }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function saveStatsCache(stats) {
|
|
158
|
-
try { fs.mkdirSync(USER_CRED_DIR, { recursive: true }); fs.writeFileSync(USER_STATS_FILE, JSON.stringify({ ...stats, _ts: Date.now() })); } catch {}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function refreshStatsCache(agentName) {
|
|
162
|
-
try {
|
|
163
|
-
const lbRes = await fetch(`${REGISTRY_URL}/api/leaderboard`, { signal: AbortSignal.timeout(5000) });
|
|
164
|
-
if (!lbRes.ok) return null;
|
|
165
|
-
const agents = await lbRes.json();
|
|
166
|
-
const idx = Array.isArray(agents) ? agents.findIndex(a => (a.agent_name || '').toLowerCase() === agentName.toLowerCase()) : -1;
|
|
167
|
-
if (idx < 0) return null;
|
|
168
|
-
const me = agents[idx];
|
|
169
|
-
const stats = { rank: idx + 1, total: agents.length, pts: me.total_points || 0, reports: me.total_reports || 0, official: !!me.is_official };
|
|
170
|
-
saveStatsCache(stats);
|
|
171
|
-
return stats;
|
|
172
|
-
} catch { return null; }
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function loadConfig() {
|
|
145
|
+
function saveCredentials(data) {
|
|
146
|
+
const json = JSON.stringify(data, null, 2);
|
|
147
|
+
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
148
|
+
fs.writeFileSync(USER_CRED_FILE, json, { mode: 0o600 });
|
|
176
149
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
150
|
+
fs.mkdirSync(path.dirname(SKILL_CRED_FILE), { recursive: true });
|
|
151
|
+
fs.writeFileSync(SKILL_CRED_FILE, json, { mode: 0o600 });
|
|
180
152
|
} catch {}
|
|
181
|
-
return {};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function saveConfig(data) {
|
|
185
|
-
const existing = loadConfig();
|
|
186
|
-
const merged = { ...existing, ...data };
|
|
187
|
-
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
188
|
-
fs.writeFileSync(USER_CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
189
153
|
}
|
|
190
154
|
|
|
191
155
|
function askQuestion(question) {
|
|
@@ -298,6 +262,77 @@ function multiSelect(items, { title = 'Select items', hint = 'Space=toggle ↑
|
|
|
298
262
|
});
|
|
299
263
|
}
|
|
300
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Interactive single-select in terminal. No dependencies.
|
|
267
|
+
* items: [{ label, sublabel?, value }]
|
|
268
|
+
* Returns: selected value (or null if cancelled)
|
|
269
|
+
*/
|
|
270
|
+
function singleSelect(items, { title = 'Select', hint = '↑↓=move Enter=select Esc=cancel' } = {}) {
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
if (!process.stdin.isTTY) {
|
|
273
|
+
resolve(items[0]?.value || null);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
|
|
279
|
+
const render = () => {
|
|
280
|
+
process.stdout.write(`\x1b[${items.length + 3}A\x1b[J`);
|
|
281
|
+
draw();
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const draw = () => {
|
|
285
|
+
console.log(` ${c.bold}${title}${c.reset}`);
|
|
286
|
+
console.log(` ${c.dim}${hint}${c.reset}`);
|
|
287
|
+
console.log();
|
|
288
|
+
for (let i = 0; i < items.length; i++) {
|
|
289
|
+
const item = items[i];
|
|
290
|
+
const isCursor = i === cursor;
|
|
291
|
+
const pointer = isCursor ? `${c.cyan}❯${c.reset}` : ' ';
|
|
292
|
+
const label = isCursor ? `${c.bold}${c.cyan}${item.label}${c.reset}` : item.label;
|
|
293
|
+
const sub = item.sublabel ? ` ${c.dim}${item.sublabel}${c.reset}` : '';
|
|
294
|
+
console.log(` ${pointer} ${label}${sub}`);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
draw();
|
|
299
|
+
|
|
300
|
+
process.stdin.setRawMode(true);
|
|
301
|
+
process.stdin.resume();
|
|
302
|
+
process.stdin.setEncoding('utf8');
|
|
303
|
+
|
|
304
|
+
const onData = (key) => {
|
|
305
|
+
if (key === '\x03' || key === '\x1b') {
|
|
306
|
+
process.stdin.setRawMode(false);
|
|
307
|
+
process.stdin.pause();
|
|
308
|
+
process.stdin.removeListener('data', onData);
|
|
309
|
+
console.log();
|
|
310
|
+
resolve(null);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (key === '\r' || key === '\n') {
|
|
314
|
+
process.stdin.setRawMode(false);
|
|
315
|
+
process.stdin.pause();
|
|
316
|
+
process.stdin.removeListener('data', onData);
|
|
317
|
+
resolve(items[cursor].value);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
321
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
322
|
+
render();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
326
|
+
cursor = (cursor + 1) % items.length;
|
|
327
|
+
render();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
process.stdin.on('data', onData);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
301
336
|
async function registerAgent(agentName) {
|
|
302
337
|
const res = await fetch(`${REGISTRY_URL}/api/register`, {
|
|
303
338
|
method: 'POST',
|
|
@@ -354,8 +389,6 @@ async function setupCommand() {
|
|
|
354
389
|
console.log(` ${icons.safe} Registered as ${c.bold}${data.agent_name}${c.reset}`);
|
|
355
390
|
console.log(` ${c.dim}Key: ${data.api_key.slice(0, 12)}...${c.reset}`);
|
|
356
391
|
console.log(` ${c.dim}Saved to: ${USER_CRED_FILE}${c.reset}`);
|
|
357
|
-
// Initialize stats cache
|
|
358
|
-
refreshStatsCache(data.agent_name).catch(() => {});
|
|
359
392
|
} catch (err) {
|
|
360
393
|
console.log(` ${c.red}failed${c.reset}`);
|
|
361
394
|
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
@@ -364,6 +397,34 @@ async function setupCommand() {
|
|
|
364
397
|
}
|
|
365
398
|
|
|
366
399
|
console.log();
|
|
400
|
+
|
|
401
|
+
// ── LLM model configuration ──
|
|
402
|
+
const llmConfig = loadLlmConfig();
|
|
403
|
+
console.log(` ${c.bold}LLM Model Configuration${c.reset}`);
|
|
404
|
+
console.log(` ${c.dim}Used for deep audits. Requires an LLM API key in your environment.${c.reset}`);
|
|
405
|
+
console.log();
|
|
406
|
+
if (llmConfig) {
|
|
407
|
+
console.log(` ${icons.safe} Current model: ${c.bold}${llmConfig.llm_model}${c.reset}`);
|
|
408
|
+
console.log();
|
|
409
|
+
}
|
|
410
|
+
console.log(` ${c.dim}Examples:${c.reset}`);
|
|
411
|
+
console.log(` ${c.dim}claude-sonnet-4-20250514${c.reset} ${c.dim}(Anthropic)${c.reset}`);
|
|
412
|
+
console.log(` ${c.dim}gpt-4o${c.reset} ${c.dim}(OpenAI)${c.reset}`);
|
|
413
|
+
console.log(` ${c.dim}gemini-2.5-flash${c.reset} ${c.dim}(Google)${c.reset}`);
|
|
414
|
+
console.log(` ${c.dim}qwen/qwen3.5-coder-32b${c.reset} ${c.dim}(OpenRouter)${c.reset}`);
|
|
415
|
+
console.log(` ${c.dim}deepseek-chat${c.reset} ${c.dim}(DeepSeek)${c.reset}`);
|
|
416
|
+
console.log();
|
|
417
|
+
const modelAnswer = await askQuestion(` Model ${c.dim}(Enter to keep default, or type model name)${c.reset}: `);
|
|
418
|
+
if (modelAnswer) {
|
|
419
|
+
saveLlmConfig(modelAnswer);
|
|
420
|
+
console.log(` ${icons.safe} Model set to ${c.bold}${modelAnswer}${c.reset}`);
|
|
421
|
+
} else if (llmConfig) {
|
|
422
|
+
console.log(` ${c.dim}Keeping: ${llmConfig.llm_model}${c.reset}`);
|
|
423
|
+
} else {
|
|
424
|
+
console.log(` ${c.dim}No custom model — will use provider default${c.reset}`);
|
|
425
|
+
}
|
|
426
|
+
console.log();
|
|
427
|
+
|
|
367
428
|
console.log(` ${c.bold}Ready!${c.reset} You can now:`);
|
|
368
429
|
console.log(` ${c.dim}•${c.reset} Discover servers: ${c.cyan}agentaudit discover${c.reset}`);
|
|
369
430
|
console.log(` ${c.dim}•${c.reset} Audit packages: ${c.cyan}agentaudit audit <repo-url>${c.reset} ${c.dim}(deep LLM analysis)${c.reset}`);
|
|
@@ -373,30 +434,27 @@ async function setupCommand() {
|
|
|
373
434
|
console.log();
|
|
374
435
|
}
|
|
375
436
|
|
|
376
|
-
// ──
|
|
437
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
377
438
|
|
|
378
|
-
function
|
|
379
|
-
|
|
380
|
-
|
|
439
|
+
function validateGitUrl(url) {
|
|
440
|
+
// Reject URLs with shell metacharacters to prevent command injection
|
|
441
|
+
if (/[;&|`$(){}!\n\r]/.test(url)) {
|
|
442
|
+
throw new Error(`Rejected URL with suspicious characters: ${url.slice(0, 80)}`);
|
|
443
|
+
}
|
|
444
|
+
// Must look like a URL (http/https/git/ssh) or a GitHub shorthand
|
|
445
|
+
if (!/^(https?:\/\/|git@|git:\/\/|ssh:\/\/)/.test(url) && !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(url)) {
|
|
446
|
+
throw new Error(`Invalid repository URL: ${url.slice(0, 80)}`);
|
|
381
447
|
}
|
|
382
|
-
process.exitCode = exitCode;
|
|
383
448
|
}
|
|
384
449
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
392
|
-
for (let i = 1; i <= m; i++)
|
|
393
|
-
for (let j = 1; j <= n; j++)
|
|
394
|
-
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));
|
|
395
|
-
return dp[m][n];
|
|
450
|
+
function safeGitClone(url, destPath, timeoutMs = 30_000) {
|
|
451
|
+
validateGitUrl(url);
|
|
452
|
+
execFileSync('git', ['clone', '--depth', '1', url, destPath], {
|
|
453
|
+
timeout: timeoutMs,
|
|
454
|
+
stdio: 'pipe',
|
|
455
|
+
});
|
|
396
456
|
}
|
|
397
457
|
|
|
398
|
-
// ── Helpers ──────────────────────────────────────────────
|
|
399
|
-
|
|
400
458
|
function getVersion() {
|
|
401
459
|
try {
|
|
402
460
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
@@ -404,38 +462,17 @@ function getVersion() {
|
|
|
404
462
|
} catch { return '0.0.0'; }
|
|
405
463
|
}
|
|
406
464
|
|
|
407
|
-
|
|
465
|
+
function banner() {
|
|
408
466
|
if (quietMode || jsonMode) return;
|
|
409
|
-
const creds = loadCredentials();
|
|
410
|
-
const agentInfo = creds?.agent_name ? ` ${c.dim}·${c.reset} ${c.green}${creds.agent_name}${c.reset}` : '';
|
|
411
467
|
console.log();
|
|
412
|
-
console.log(`
|
|
413
|
-
|
|
414
|
-
let cached = loadStatsCache();
|
|
415
|
-
// Auto-fetch if no cache or cache older than 1h
|
|
416
|
-
if (!cached || !cached._ts || (Date.now() - cached._ts > 3600000)) {
|
|
417
|
-
cached = await refreshStatsCache(creds.agent_name).catch(() => null) || cached;
|
|
418
|
-
}
|
|
419
|
-
if (cached && cached.rank) {
|
|
420
|
-
const medal = cached.rank === 1 ? '🥇' : cached.rank === 2 ? '🥈' : cached.rank === 3 ? '🥉' : `#${cached.rank}`;
|
|
421
|
-
console.log(` ${c.dim}${medal} of ${cached.total} · ${c.reset}${c.cyan}${cached.pts}${c.reset}${c.dim} pts · ${cached.reports} audits${c.reset}`);
|
|
422
|
-
}
|
|
423
|
-
} else {
|
|
424
|
-
console.log(` ${c.dim}Security scanner for AI tools${c.reset}`);
|
|
425
|
-
}
|
|
468
|
+
console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
|
|
469
|
+
console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
|
|
426
470
|
console.log();
|
|
427
471
|
}
|
|
428
472
|
|
|
429
473
|
function slugFromUrl(url) {
|
|
430
474
|
const match = url.match(/github\.com\/([^/]+)\/([^/.\s]+)/);
|
|
431
|
-
if (match)
|
|
432
|
-
const owner = match[1].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
433
|
-
const repo = match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
434
|
-
// Generic repo names get owner prefix to avoid collisions
|
|
435
|
-
const generic = ['mcp', 'server', 'plugin', 'tool', 'agent', 'sdk', 'api', 'app', 'cli', 'lib', 'core'];
|
|
436
|
-
if (generic.includes(repo)) return `${owner}-${repo}`;
|
|
437
|
-
return repo;
|
|
438
|
-
}
|
|
475
|
+
if (match) return match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
439
476
|
return url.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60);
|
|
440
477
|
}
|
|
441
478
|
|
|
@@ -474,6 +511,46 @@ function severityIcon(sev) {
|
|
|
474
511
|
|
|
475
512
|
// ── File Collection (same logic as MCP server) ──────────
|
|
476
513
|
|
|
514
|
+
function formatApiError(error, provider, statusCode) {
|
|
515
|
+
// Extract error message from various API response formats
|
|
516
|
+
const msg = (typeof error === 'string' ? error : error?.message || error?.error?.message || JSON.stringify(error)).toLowerCase();
|
|
517
|
+
|
|
518
|
+
// Authentication errors
|
|
519
|
+
if (statusCode === 401 || statusCode === 403 || msg.includes('invalid api key') || msg.includes('invalid x-api-key') ||
|
|
520
|
+
msg.includes('incorrect api key') || msg.includes('authentication') || msg.includes('unauthorized') ||
|
|
521
|
+
msg.includes('invalid_api_key') || msg.includes('permission denied')) {
|
|
522
|
+
return { text: 'Invalid or expired API key', hint: `Check your ${provider} API key. Run: echo $${provider}_API_KEY` };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Rate limits / quota
|
|
526
|
+
if (statusCode === 429 || msg.includes('rate limit') || msg.includes('rate_limit') || msg.includes('too many requests') ||
|
|
527
|
+
msg.includes('quota') || msg.includes('insufficient_quota') || msg.includes('billing') ||
|
|
528
|
+
msg.includes('exceeded') || msg.includes('no credits') || msg.includes('credit') ||
|
|
529
|
+
msg.includes('overloaded') || msg.includes('capacity')) {
|
|
530
|
+
return { text: 'Rate limit or quota exceeded', hint: 'Wait a moment and retry, or check your billing/credits at your provider dashboard' };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Model not found
|
|
534
|
+
if (statusCode === 404 || msg.includes('not found') || msg.includes('not a valid model') ||
|
|
535
|
+
msg.includes('model_not_found') || msg.includes('does not exist') || msg.includes('invalid model')) {
|
|
536
|
+
return { text: 'Model not found', hint: `"${msg}" — check model name with: agentaudit model` };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Context length / payload too large
|
|
540
|
+
if (statusCode === 413 || msg.includes('context length') || msg.includes('too long') ||
|
|
541
|
+
msg.includes('maximum') || msg.includes('token limit') || msg.includes('content_too_large')) {
|
|
542
|
+
return { text: 'Input too large for model', hint: 'The repository has too many files. Try a smaller repo or a model with larger context window' };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Server errors
|
|
546
|
+
if (statusCode >= 500) {
|
|
547
|
+
return { text: `Provider server error (HTTP ${statusCode})`, hint: `${provider} might be experiencing issues. Try again later` };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Fallback
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
477
554
|
function extractJSON(text) {
|
|
478
555
|
// 1. Try parsing the entire text as JSON directly
|
|
479
556
|
try { return JSON.parse(text.trim()); } catch {}
|
|
@@ -526,60 +603,36 @@ function extractJSON(text) {
|
|
|
526
603
|
|
|
527
604
|
const MAX_FILE_SIZE = 50_000;
|
|
528
605
|
const MAX_TOTAL_SIZE = 300_000;
|
|
529
|
-
// Directories safe to skip: dependencies, caches, build artifacts, editor config.
|
|
530
|
-
// SECURITY RULE: If a directory can contain source code, workflow files, or
|
|
531
|
-
// prose (prompt injection), it MUST be scanned.
|
|
532
|
-
// Reviewed 2026-02-17:
|
|
533
|
-
// - RESTORED: test/tests/__tests__/spec/specs/e2e (malware in test hooks)
|
|
534
|
-
// - RESTORED: .github (workflow injection, supply chain attacks)
|
|
535
|
-
// - RESTORED: examples/example (hidden backdoors in "example" code)
|
|
536
|
-
// - RESTORED: docs/doc (prompt injection in documentation)
|
|
537
|
-
// - RESTORED: fixtures (test data can contain malicious payloads)
|
|
538
606
|
const SKIP_DIRS = new Set([
|
|
539
|
-
'node_modules', '.git', '__pycache__', '.venv', 'venv', '
|
|
540
|
-
'
|
|
541
|
-
'
|
|
542
|
-
'.vscode', '.idea',
|
|
607
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
608
|
+
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
609
|
+
'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
|
|
610
|
+
'examples', 'example', 'fixtures', '.github', '.vscode', '.idea',
|
|
611
|
+
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
543
612
|
]);
|
|
544
613
|
const SKIP_EXTENSIONS = new Set([
|
|
545
614
|
'.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff',
|
|
546
615
|
'.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.tar', '.gz',
|
|
547
616
|
'.map', '.min.js', '.min.css', '.d.ts', '.pyc', '.pyo', '.so',
|
|
548
617
|
'.dylib', '.dll', '.exe', '.bin', '.dat', '.db', '.sqlite',
|
|
549
|
-
'.snap', '.patch', '.diff', '.log', '.csv', '.tsv', '.parquet',
|
|
550
|
-
]);
|
|
551
|
-
// Files safe to skip: ONLY inert line-based config that cannot execute code
|
|
552
|
-
// and cannot contain prompt injections (no prose, no markdown, no scripts).
|
|
553
|
-
// SECURITY RULE: When in doubt, SCAN IT. False positives > false negatives.
|
|
554
|
-
// Reviewed 2026-02-17: All .md files removed from skip list (prompt injection vector).
|
|
555
|
-
// All executable configs removed (malware vector). Only line-based dotfiles remain.
|
|
556
|
-
const SKIP_FILES = new Set([
|
|
557
|
-
'.gitignore', '.gitattributes', '.npmignore', '.dockerignore',
|
|
558
|
-
'.editorconfig', '.browserslistrc', '.nvmrc', '.node-version',
|
|
559
|
-
'.prettierignore', '.eslintignore',
|
|
560
618
|
]);
|
|
561
619
|
|
|
562
|
-
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
563
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE)
|
|
620
|
+
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
621
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
564
622
|
let entries;
|
|
565
623
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
566
624
|
catch { return collected; }
|
|
567
625
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
568
626
|
for (const entry of entries) {
|
|
627
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
|
|
569
628
|
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
570
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE) { totalSize.truncated = true; totalSize.skippedPaths.push(relPath); continue; }
|
|
571
629
|
const fullPath = path.join(dir, entry.name);
|
|
572
|
-
// SECURITY: Never follow symlinks — attacker could link to /etc/passwd or ~/.ssh/
|
|
573
|
-
if (entry.isSymbolicLink()) continue;
|
|
574
630
|
if (entry.isDirectory()) {
|
|
575
|
-
|
|
576
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
577
|
-
if (entry.name.startsWith('.') && entry.name !== '.github') continue;
|
|
631
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
578
632
|
collectFiles(fullPath, relPath, collected, totalSize);
|
|
579
633
|
} else {
|
|
580
634
|
const ext = path.extname(entry.name).toLowerCase();
|
|
581
635
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
582
|
-
if (SKIP_FILES.has(entry.name.toLowerCase())) continue;
|
|
583
636
|
try {
|
|
584
637
|
const stat = fs.statSync(fullPath);
|
|
585
638
|
if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
|
|
@@ -924,10 +977,7 @@ async function scanRepo(url) {
|
|
|
924
977
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
925
978
|
const repoPath = path.join(tmpDir, 'repo');
|
|
926
979
|
try {
|
|
927
|
-
|
|
928
|
-
timeout: 30_000,
|
|
929
|
-
stdio: 'pipe',
|
|
930
|
-
});
|
|
980
|
+
safeGitClone(url, repoPath);
|
|
931
981
|
} catch (err) {
|
|
932
982
|
if (!jsonMode) {
|
|
933
983
|
process.stdout.write(` ${c.red}✖ clone failed${c.reset}\n`);
|
|
@@ -1437,9 +1487,7 @@ async function auditRepo(url) {
|
|
|
1437
1487
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
1438
1488
|
const repoPath = path.join(tmpDir, 'repo');
|
|
1439
1489
|
try {
|
|
1440
|
-
|
|
1441
|
-
timeout: 30_000, stdio: 'pipe',
|
|
1442
|
-
});
|
|
1490
|
+
safeGitClone(url, repoPath);
|
|
1443
1491
|
console.log(` ${c.green}done${c.reset}`);
|
|
1444
1492
|
} catch (err) {
|
|
1445
1493
|
console.log(` ${c.red}failed${c.reset}`);
|
|
@@ -1449,161 +1497,62 @@ async function auditRepo(url) {
|
|
|
1449
1497
|
return null;
|
|
1450
1498
|
}
|
|
1451
1499
|
|
|
1452
|
-
// Capture version + commit from cloned repo
|
|
1453
|
-
let repoCommitSha = null;
|
|
1454
|
-
let repoPackageVersion = null;
|
|
1455
|
-
try { repoCommitSha = execSync('git rev-parse HEAD', { cwd: repoPath, stdio: 'pipe' }).toString().trim(); } catch {}
|
|
1456
|
-
try {
|
|
1457
|
-
const pkgPath = path.join(repoPath, 'package.json');
|
|
1458
|
-
if (fs.existsSync(pkgPath)) {
|
|
1459
|
-
repoPackageVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version || null;
|
|
1460
|
-
}
|
|
1461
|
-
} catch {}
|
|
1462
|
-
|
|
1463
1500
|
// Step 2: Collect files
|
|
1464
1501
|
process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
|
|
1465
|
-
const
|
|
1466
|
-
const files = collectFiles(repoPath, '', [], _collectMeta);
|
|
1502
|
+
const files = collectFiles(repoPath);
|
|
1467
1503
|
console.log(` ${c.green}${files.length} files${c.reset}`);
|
|
1468
|
-
if (_collectMeta.truncated) {
|
|
1469
|
-
console.log(` ${c.yellow}⚠ Size limit reached (${(MAX_TOTAL_SIZE / 1000).toFixed(0)}KB) — ${_collectMeta.skippedPaths.length} files NOT collected:${c.reset}`);
|
|
1470
|
-
const shown = _collectMeta.skippedPaths.slice(0, 5);
|
|
1471
|
-
for (const p of shown) console.log(` ${c.dim} • ${p}${c.reset}`);
|
|
1472
|
-
if (_collectMeta.skippedPaths.length > 5) console.log(` ${c.dim} ... and ${_collectMeta.skippedPaths.length - 5} more${c.reset}`);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// Step 3: Resolve provider + model FIRST (needed for dynamic chunk sizing)
|
|
1476
|
-
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
1477
|
-
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1478
|
-
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
|
1479
|
-
const openrouterModel = process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
1480
|
-
const providerFlag = process.argv.find(a => a.startsWith('--provider='))?.split('=')[1]?.toLowerCase()
|
|
1481
|
-
|| (process.argv.includes('--provider') ? process.argv[process.argv.indexOf('--provider') + 1]?.toLowerCase() : null);
|
|
1482
|
-
const resolvedProvider = resolveProvider(providerFlag, { anthropicKey, openaiKey, openrouterKey });
|
|
1483
|
-
// Determine actual model name
|
|
1484
|
-
let actualModel;
|
|
1485
|
-
if (!resolvedProvider) {
|
|
1486
|
-
actualModel = 'unknown';
|
|
1487
|
-
} else if (resolvedProvider.id === 'anthropic') {
|
|
1488
|
-
actualModel = modelOverride || 'claude-sonnet-4-20250514';
|
|
1489
|
-
} else if (resolvedProvider.id === 'openrouter') {
|
|
1490
|
-
actualModel = modelOverride || process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
1491
|
-
} else if (resolvedProvider.id === 'openai') {
|
|
1492
|
-
actualModel = modelOverride || 'gpt-4o';
|
|
1493
|
-
} else if (resolvedProvider.id === 'ollama') {
|
|
1494
|
-
actualModel = modelOverride || resolvedProvider.model;
|
|
1495
|
-
} else {
|
|
1496
|
-
actualModel = modelOverride || resolvedProvider.model || 'unknown';
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// Step 3b: Determine model context for dynamic chunk sizing
|
|
1500
|
-
let modelContextTokens = 64_000; // conservative default
|
|
1501
|
-
let outputTokenBudget = 4096;
|
|
1502
1504
|
|
|
1503
|
-
|
|
1504
|
-
if (resolvedProvider.id === 'openrouter') {
|
|
1505
|
-
try {
|
|
1506
|
-
const modelInfoRes = await fetch(`https://openrouter.ai/api/v1/models`, {
|
|
1507
|
-
signal: AbortSignal.timeout(5000),
|
|
1508
|
-
headers: { 'HTTP-Referer': 'https://agentaudit.dev' },
|
|
1509
|
-
});
|
|
1510
|
-
if (modelInfoRes.ok) {
|
|
1511
|
-
const modelData = await modelInfoRes.json();
|
|
1512
|
-
const modelInfo = modelData.data?.find(m => m.id === actualModel);
|
|
1513
|
-
if (modelInfo?.context_length) {
|
|
1514
|
-
modelContextTokens = modelInfo.context_length;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
} catch { /* ignore — use default */ }
|
|
1518
|
-
} else if (resolvedProvider.id === 'anthropic') {
|
|
1519
|
-
modelContextTokens = 200_000;
|
|
1520
|
-
} else if (resolvedProvider.id === 'openai') {
|
|
1521
|
-
modelContextTokens = 128_000;
|
|
1522
|
-
} else if (resolvedProvider.id === 'ollama') {
|
|
1523
|
-
modelContextTokens = 32_000;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
outputTokenBudget = modelContextTokens >= 128_000 ? 8192 : modelContextTokens >= 64_000 ? 4096 : modelContextTokens >= 32_000 ? 2048 : 2048;
|
|
1528
|
-
const dynamicChunkChars = Math.floor(modelContextTokens * 0.5 * 4);
|
|
1529
|
-
const MAX_CHUNK_CHARS = Math.max(40_000, Math.min(dynamicChunkChars, 600_000));
|
|
1530
|
-
|
|
1531
|
-
// Step 3c: Build audit payload
|
|
1505
|
+
// Step 3: Build audit payload
|
|
1532
1506
|
process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
|
|
1533
1507
|
const auditPrompt = loadAuditPrompt();
|
|
1534
|
-
// Sort files by directory to keep related files in the same chunk.
|
|
1535
|
-
// This preserves cross-file context (imports, shared modules) within each pass.
|
|
1536
|
-
const sortedFiles = [...files].sort((a, b) => {
|
|
1537
|
-
const dirA = a.path.includes('/') ? a.path.substring(0, a.path.lastIndexOf('/')) : '';
|
|
1538
|
-
const dirB = b.path.includes('/') ? b.path.substring(0, b.path.lastIndexOf('/')) : '';
|
|
1539
|
-
return dirA.localeCompare(dirB) || a.path.localeCompare(b.path);
|
|
1540
|
-
});
|
|
1541
|
-
const chunks = []; // array of code block strings
|
|
1542
|
-
const chunkFileNames = []; // track which files are in each chunk for error reporting
|
|
1543
|
-
let currentChunk = '';
|
|
1544
|
-
let currentChars = 0;
|
|
1545
|
-
let currentFiles = [];
|
|
1546
|
-
for (const file of sortedFiles) {
|
|
1547
|
-
const entry = `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
1548
|
-
if (currentChars + entry.length > MAX_CHUNK_CHARS && currentChars > 0) {
|
|
1549
|
-
chunks.push(currentChunk);
|
|
1550
|
-
chunkFileNames.push([...currentFiles]);
|
|
1551
|
-
currentFiles = [];
|
|
1552
|
-
currentChunk = '';
|
|
1553
|
-
currentChars = 0;
|
|
1554
|
-
}
|
|
1555
|
-
// If a single file exceeds chunk limit, truncate it
|
|
1556
|
-
if (entry.length > MAX_CHUNK_CHARS) {
|
|
1557
|
-
const truncContent = file.content.substring(0, MAX_CHUNK_CHARS - 200);
|
|
1558
|
-
currentChunk += `\n### FILE: ${file.path}\n\`\`\`\n${truncContent}\n[... file truncated, ${file.content.length} chars total ...]\n\`\`\`\n`;
|
|
1559
|
-
currentChars += MAX_CHUNK_CHARS;
|
|
1560
|
-
} else {
|
|
1561
|
-
currentChunk += entry;
|
|
1562
|
-
currentChars += entry.length;
|
|
1563
|
-
}
|
|
1564
|
-
currentFiles.push(file.path);
|
|
1565
|
-
}
|
|
1566
|
-
if (currentChunk) { chunks.push(currentChunk); chunkFileNames.push([...currentFiles]); }
|
|
1567
1508
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
} else {
|
|
1572
|
-
console.log(` ${c.green}done${c.reset}`);
|
|
1509
|
+
let codeBlock = '';
|
|
1510
|
+
for (const file of files) {
|
|
1511
|
+
codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
1573
1512
|
}
|
|
1574
|
-
|
|
1575
|
-
const codeBlock = chunks[0] || '';
|
|
1513
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
1576
1514
|
|
|
1577
1515
|
// Step 4: LLM Analysis
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1516
|
+
// Detect provider from environment variables (first match wins)
|
|
1517
|
+
const activeLlm = LLM_PROVIDERS.find(p => process.env[p.key]);
|
|
1518
|
+
const llmApiKey = activeLlm ? process.env[activeLlm.key] : null;
|
|
1519
|
+
const activeProvider = activeLlm ? activeLlm.name : null;
|
|
1520
|
+
|
|
1521
|
+
// Model override: --model flag > AGENTAUDIT_MODEL env > credentials.json > provider default
|
|
1522
|
+
const modelArgIdx = process.argv.indexOf('--model');
|
|
1523
|
+
const modelFlag = modelArgIdx !== -1 ? process.argv[modelArgIdx + 1] : null;
|
|
1524
|
+
const modelEnv = process.env.AGENTAUDIT_MODEL;
|
|
1525
|
+
const modelConfig = loadLlmConfig()?.llm_model;
|
|
1526
|
+
const modelOverride = modelFlag || modelEnv || modelConfig || null;
|
|
1527
|
+
if (activeLlm && modelOverride) {
|
|
1528
|
+
activeLlm.model = modelOverride;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (!activeLlm) {
|
|
1581
1532
|
// No LLM API key — clear explanation
|
|
1582
1533
|
console.log();
|
|
1583
|
-
console.log(` ${c.yellow}No LLM
|
|
1534
|
+
console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
|
|
1584
1535
|
console.log();
|
|
1585
|
-
console.log(` ${c.bold}Option 1: Set an API key${c.reset}
|
|
1586
|
-
console.log(` ${c.cyan}ANTHROPIC_API_KEY${c.reset} Anthropic Claude
|
|
1536
|
+
console.log(` ${c.bold}Option 1: Set an API key${c.reset} (any one of these):`);
|
|
1537
|
+
console.log(` ${c.cyan}ANTHROPIC_API_KEY${c.reset} Anthropic Claude`);
|
|
1587
1538
|
console.log(` ${c.cyan}OPENAI_API_KEY${c.reset} OpenAI GPT-4o`);
|
|
1588
|
-
console.log(` ${c.cyan}
|
|
1589
|
-
console.log(` ${c.cyan}
|
|
1590
|
-
console.log(` ${c.cyan}
|
|
1539
|
+
console.log(` ${c.cyan}GEMINI_API_KEY${c.reset} Google Gemini`);
|
|
1540
|
+
console.log(` ${c.cyan}DEEPSEEK_API_KEY${c.reset} DeepSeek`);
|
|
1541
|
+
console.log(` ${c.cyan}MISTRAL_API_KEY${c.reset} Mistral`);
|
|
1542
|
+
console.log(` ${c.cyan}XAI_API_KEY${c.reset} xAI Grok`);
|
|
1543
|
+
console.log(` ${c.cyan}GROQ_API_KEY${c.reset} Groq`);
|
|
1544
|
+
console.log(` ${c.cyan}TOGETHER_API_KEY${c.reset} Together AI`);
|
|
1545
|
+
console.log(` ${c.cyan}FIREWORKS_API_KEY${c.reset} Fireworks AI`);
|
|
1546
|
+
console.log(` ${c.cyan}CEREBRAS_API_KEY${c.reset} Cerebras`);
|
|
1547
|
+
console.log(` ${c.cyan}ZAI_API_KEY${c.reset} Zhipu AI (GLM)`);
|
|
1548
|
+
console.log(` ${c.cyan}OPENROUTER_API_KEY${c.reset} OpenRouter (any model)`);
|
|
1591
1549
|
console.log();
|
|
1592
|
-
console.log(` ${c.dim}# Linux / macOS:${c.reset}`);
|
|
1593
1550
|
console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
1594
1551
|
console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
|
|
1595
1552
|
console.log();
|
|
1596
|
-
console.log(` ${c.dim}# Windows (PowerShell):${c.reset}`);
|
|
1597
|
-
console.log(` ${c.dim}$env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
|
|
1598
|
-
console.log(` ${c.dim}$env:OPENAI_API_KEY = "sk-..."${c.reset}`);
|
|
1599
|
-
console.log();
|
|
1600
|
-
console.log(` ${c.dim}# Windows (CMD):${c.reset}`);
|
|
1601
|
-
console.log(` ${c.dim}set ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
1602
|
-
console.log(` ${c.dim}set OPENAI_API_KEY=sk-...${c.reset}`);
|
|
1603
|
-
console.log();
|
|
1604
1553
|
console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
|
|
1605
1554
|
console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
|
|
1606
|
-
console.log(` ${c.dim}Creates a markdown file you can paste into any LLM
|
|
1555
|
+
console.log(` ${c.dim}Creates a markdown file you can paste into any LLM${c.reset}`);
|
|
1607
1556
|
console.log();
|
|
1608
1557
|
console.log(` ${c.bold}Option 3: Use MCP in Claude/Cursor/Windsurf (no API key needed)${c.reset}`);
|
|
1609
1558
|
console.log(` ${c.dim}Add AgentAudit as MCP server — your editor's agent runs the audit using its own LLM.${c.reset}`);
|
|
@@ -1643,319 +1592,181 @@ async function auditRepo(url) {
|
|
|
1643
1592
|
return null;
|
|
1644
1593
|
}
|
|
1645
1594
|
|
|
1646
|
-
//
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
let _lastLlmText = '';
|
|
1665
|
-
let result = null;
|
|
1666
|
-
let meta = {};
|
|
1667
|
-
|
|
1668
|
-
try {
|
|
1669
|
-
if (resolvedProvider.id === 'anthropic') {
|
|
1670
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
1671
|
-
method: 'POST',
|
|
1672
|
-
headers: {
|
|
1673
|
-
'x-api-key': resolvedProvider.key,
|
|
1674
|
-
'anthropic-version': '2023-06-01',
|
|
1675
|
-
'content-type': 'application/json',
|
|
1676
|
-
},
|
|
1677
|
-
body: JSON.stringify({
|
|
1678
|
-
model: modelOverride || 'claude-sonnet-4-20250514',
|
|
1679
|
-
max_tokens: outputTokenBudget,
|
|
1680
|
-
system: systemPrompt,
|
|
1681
|
-
messages: [{ role: 'user', content: userMessage }],
|
|
1682
|
-
}),
|
|
1683
|
-
signal: AbortSignal.timeout(llmTimeoutMs || 180_000),
|
|
1684
|
-
});
|
|
1685
|
-
const data = await res.json();
|
|
1686
|
-
if (data.error) {
|
|
1687
|
-
return { error: data.error.message || JSON.stringify(data.error) };
|
|
1688
|
-
}
|
|
1689
|
-
const text = data.content?.[0]?.text || '';
|
|
1690
|
-
_lastLlmText = text;
|
|
1691
|
-
result = extractJSON(text);
|
|
1692
|
-
meta = {
|
|
1693
|
-
provider_msg_id: data.id || null,
|
|
1694
|
-
input_tokens: data.usage?.input_tokens || null,
|
|
1695
|
-
output_tokens: data.usage?.output_tokens || null,
|
|
1696
|
-
reported_model: data.model || null,
|
|
1697
|
-
};
|
|
1698
|
-
} else {
|
|
1699
|
-
let apiUrl, modelName, authHeaders;
|
|
1700
|
-
switch (resolvedProvider.id) {
|
|
1701
|
-
case 'openrouter':
|
|
1702
|
-
apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
|
|
1703
|
-
modelName = modelOverride || process.env.OPENROUTER_MODEL || 'anthropic/claude-sonnet-4';
|
|
1704
|
-
authHeaders = { 'Authorization': `Bearer ${resolvedProvider.key}`, 'HTTP-Referer': 'https://agentaudit.dev', 'X-Title': 'AgentAudit' };
|
|
1705
|
-
break;
|
|
1706
|
-
case 'ollama':
|
|
1707
|
-
apiUrl = `${resolvedProvider.host}/v1/chat/completions`;
|
|
1708
|
-
modelName = modelOverride || resolvedProvider.model;
|
|
1709
|
-
authHeaders = {};
|
|
1710
|
-
break;
|
|
1711
|
-
case 'custom':
|
|
1712
|
-
apiUrl = resolvedProvider.url.endsWith('/chat/completions') ? resolvedProvider.url : `${resolvedProvider.url.replace(/\/$/, '')}/chat/completions`;
|
|
1713
|
-
modelName = modelOverride || resolvedProvider.model;
|
|
1714
|
-
authHeaders = resolvedProvider.key ? { 'Authorization': `Bearer ${resolvedProvider.key}` } : {};
|
|
1715
|
-
break;
|
|
1716
|
-
default:
|
|
1717
|
-
apiUrl = 'https://api.openai.com/v1/chat/completions';
|
|
1718
|
-
modelName = modelOverride || 'gpt-4o';
|
|
1719
|
-
authHeaders = { 'Authorization': `Bearer ${resolvedProvider.key}` };
|
|
1720
|
-
}
|
|
1595
|
+
// We have an API key — run LLM audit
|
|
1596
|
+
const modelLabel = modelOverride ? `${activeProvider} → ${activeLlm.model}` : activeProvider;
|
|
1597
|
+
process.stdout.write(` ${c.dim}[4/4]${c.reset} Running LLM analysis ${c.dim}(${modelLabel})${c.reset}...`);
|
|
1598
|
+
|
|
1599
|
+
const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
|
|
1600
|
+
const userMessage = [
|
|
1601
|
+
`Audit this package: **${slug}** (${url})`,
|
|
1602
|
+
``,
|
|
1603
|
+
`After analysis, respond with ONLY a valid JSON object. No markdown fences, no explanation, no text before or after. Just the raw JSON:`,
|
|
1604
|
+
`{ "skill_slug": "${slug}", "source_url": "${url}", "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
|
|
1605
|
+
` "risk_score": <0-100>, "result": "<safe|caution|unsafe>", "max_severity": "<none|low|medium|high|critical>",`,
|
|
1606
|
+
` "findings_count": <n>, "findings": [{ "id": "...", "title": "...", "severity": "...", "category": "...",`,
|
|
1607
|
+
` "description": "...", "file": "...", "line": <n>, "remediation": "...", "confidence": "...", "is_by_design": false }] }`,
|
|
1608
|
+
``,
|
|
1609
|
+
`## Source Code`,
|
|
1610
|
+
codeBlock,
|
|
1611
|
+
].join('\n');
|
|
1721
1612
|
|
|
1722
|
-
const res = await fetch(apiUrl, {
|
|
1723
|
-
method: 'POST',
|
|
1724
|
-
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
1725
|
-
body: JSON.stringify({
|
|
1726
|
-
model: modelName,
|
|
1727
|
-
max_tokens: outputTokenBudget,
|
|
1728
|
-
messages: [
|
|
1729
|
-
{ role: 'system', content: systemPrompt },
|
|
1730
|
-
{ role: 'user', content: userMessage },
|
|
1731
|
-
],
|
|
1732
|
-
}),
|
|
1733
|
-
signal: AbortSignal.timeout(llmTimeoutMs || (resolvedProvider.id === 'ollama' ? 300_000 : 180_000)),
|
|
1734
|
-
});
|
|
1735
|
-
const data = await res.json();
|
|
1736
|
-
if (data.error) {
|
|
1737
|
-
return { error: data.error.message || JSON.stringify(data.error) };
|
|
1738
|
-
}
|
|
1739
|
-
const text = data.choices?.[0]?.message?.content || '';
|
|
1740
|
-
_lastLlmText = text;
|
|
1741
|
-
result = extractJSON(text);
|
|
1742
|
-
meta = {
|
|
1743
|
-
provider_msg_id: data.id || null,
|
|
1744
|
-
provider_fingerprint: data.system_fingerprint || null,
|
|
1745
|
-
input_tokens: data.usage?.prompt_tokens || null,
|
|
1746
|
-
output_tokens: data.usage?.completion_tokens || null,
|
|
1747
|
-
reported_model: data.model || null,
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
} catch (err) {
|
|
1751
|
-
return { error: err.message };
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
return { report: result, meta, rawText: _lastLlmText };
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
// ── Run LLM analysis (single or multi-pass) ──
|
|
1758
1613
|
let report = null;
|
|
1759
|
-
let providerMeta = {};
|
|
1760
1614
|
let _lastLlmText = '';
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1615
|
+
|
|
1616
|
+
try {
|
|
1617
|
+
let data;
|
|
1618
|
+
if (activeLlm.type === 'anthropic') {
|
|
1619
|
+
// Anthropic Messages API (unique format)
|
|
1620
|
+
const res = await fetch(activeLlm.url, {
|
|
1621
|
+
method: 'POST',
|
|
1622
|
+
headers: {
|
|
1623
|
+
'x-api-key': llmApiKey,
|
|
1624
|
+
'anthropic-version': '2023-06-01',
|
|
1625
|
+
'content-type': 'application/json',
|
|
1626
|
+
},
|
|
1627
|
+
body: JSON.stringify({
|
|
1628
|
+
model: activeLlm.model,
|
|
1629
|
+
max_tokens: 8192,
|
|
1630
|
+
system: systemPrompt,
|
|
1631
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
1632
|
+
}),
|
|
1633
|
+
signal: AbortSignal.timeout(120_000),
|
|
1634
|
+
});
|
|
1635
|
+
data = await res.json();
|
|
1636
|
+
if (data.error) {
|
|
1637
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1638
|
+
const friendly = formatApiError(data.error, activeLlm.provider, res.status);
|
|
1639
|
+
if (friendly) {
|
|
1640
|
+
console.log(` ${c.red}${friendly.text}${c.reset}`);
|
|
1641
|
+
console.log(` ${c.dim}${friendly.hint}${c.reset}`);
|
|
1642
|
+
} else {
|
|
1643
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1780
1644
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
if (!result.report) {
|
|
1785
|
-
console.log(` ${c.yellow}no findings (empty/unparseable)${c.reset}`);
|
|
1786
|
-
_lastLlmText = result.rawText || '';
|
|
1787
|
-
continue;
|
|
1645
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1646
|
+
return null;
|
|
1788
1647
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
if (!baseReport) {
|
|
1802
|
-
console.log(` ${c.red}✖ All passes failed to produce a report${c.reset}`);
|
|
1803
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1804
|
-
return null;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// Merge: deduplicate findings by title+file, recalculate risk score
|
|
1808
|
-
const seen = new Set();
|
|
1809
|
-
const mergedFindings = [];
|
|
1810
|
-
for (const f of allFindings) {
|
|
1811
|
-
const key = `${f.title}::${f.file || ''}`;
|
|
1812
|
-
if (!seen.has(key)) {
|
|
1813
|
-
seen.add(key);
|
|
1814
|
-
mergedFindings.push(f);
|
|
1648
|
+
_lastLlmText = data.content?.[0]?.text || '';
|
|
1649
|
+
report = extractJSON(_lastLlmText);
|
|
1650
|
+
if (report) {
|
|
1651
|
+
report.audit_model = data.model || activeLlm.model;
|
|
1652
|
+
report.audit_provider = activeLlm.provider;
|
|
1653
|
+
if (data.id) report.provider_msg_id = data.id;
|
|
1654
|
+
if (data.usage) {
|
|
1655
|
+
report.input_tokens = data.usage.input_tokens;
|
|
1656
|
+
report.output_tokens = data.usage.output_tokens;
|
|
1657
|
+
}
|
|
1815
1658
|
}
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
console.log(` ${c.dim} Merged: ${mergedFindings.length} unique findings from ${chunks.length} passes${c.reset}`);
|
|
1837
|
-
|
|
1838
|
-
// ── Cross-file correlation pass ──
|
|
1839
|
-
// Build lightweight import/export map and ask LLM to check for multi-file attack patterns
|
|
1840
|
-
// that individual chunk passes couldn't detect (e.g., credential read in file A + exfil in file B)
|
|
1841
|
-
process.stdout.write(` ${c.dim} Cross-file correlation...${c.reset}`);
|
|
1842
|
-
try {
|
|
1843
|
-
const importMap = sortedFiles.map(f => {
|
|
1844
|
-
const imports = [];
|
|
1845
|
-
const exports = [];
|
|
1846
|
-
// JS/TS imports
|
|
1847
|
-
for (const m of f.content.matchAll(/(?:import|require)\s*\(?['"]([^'"]+)['"]\)?/g)) imports.push(m[1]);
|
|
1848
|
-
for (const m of f.content.matchAll(/(?:from)\s+['"]([^'"]+)['"]/g)) imports.push(m[1]);
|
|
1849
|
-
// Python imports
|
|
1850
|
-
for (const m of f.content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(m[1]);
|
|
1851
|
-
// Exports
|
|
1852
|
-
for (const m of f.content.matchAll(/(?:module\.exports|export\s+(?:default\s+)?(?:function|class|const|let|var)\s+)(\w+)/g)) exports.push(m[1]);
|
|
1853
|
-
// Dangerous function calls (brief)
|
|
1854
|
-
const dangerousCalls = [];
|
|
1855
|
-
if (/\b(?:exec|spawn|execSync|system|eval|Function)\s*\(/.test(f.content)) dangerousCalls.push('exec/eval');
|
|
1856
|
-
if (/\b(?:fetch|https?\.request|axios|got)\s*\(/.test(f.content)) dangerousCalls.push('network');
|
|
1857
|
-
if (/\b(?:readFile|writeFile|createReadStream|open)\s*\(/.test(f.content)) dangerousCalls.push('fs');
|
|
1858
|
-
if (/process\.env|os\.environ|getenv/.test(f.content)) dangerousCalls.push('env-read');
|
|
1859
|
-
return { path: f.path, imports: [...new Set(imports)].slice(0, 10), exports: exports.slice(0, 10), calls: dangerousCalls };
|
|
1860
|
-
}).filter(f => f.imports.length > 0 || f.exports.length > 0 || f.calls.length > 0);
|
|
1861
|
-
|
|
1862
|
-
if (importMap.length > 2) {
|
|
1863
|
-
const correlationPrompt = [
|
|
1864
|
-
`You previously analyzed ${chunks.length} code chunks from package "${slug}" (${url}).`,
|
|
1865
|
-
`Here is a cross-file map showing imports, exports, and dangerous function calls.`,
|
|
1866
|
-
`Check for MULTI-FILE ATTACK PATTERNS that individual chunk analysis could miss:`,
|
|
1867
|
-
`- File A reads credentials/env → File B sends them to network (credential exfiltration pipeline)`,
|
|
1868
|
-
`- File A defines a function with exec/eval → File B calls it with user input (indirect RCE)`,
|
|
1869
|
-
`- Config file grants broad permissions → Code file exploits them`,
|
|
1870
|
-
`- Install hook in scripts/ triggers code in src/ that exfiltrates data`,
|
|
1871
|
-
``,
|
|
1872
|
-
`Respond with ONLY a JSON object: { "cross_file_findings": [...] } where each finding has:`,
|
|
1873
|
-
`{ "title": "...", "severity": "...", "description": "...", "file": "...", "confidence": "...", "pattern_id": "CORR_001", "remediation": "..." }`,
|
|
1874
|
-
`If no cross-file issues found, respond: { "cross_file_findings": [] }`,
|
|
1875
|
-
``,
|
|
1876
|
-
`## File Map`,
|
|
1877
|
-
JSON.stringify(importMap, null, 2),
|
|
1878
|
-
].join('\n');
|
|
1879
|
-
|
|
1880
|
-
const corrResult = await callLLM(correlationPrompt, 'correlation');
|
|
1881
|
-
if (corrResult.report?.cross_file_findings?.length > 0) {
|
|
1882
|
-
const corrFindings = corrResult.report.cross_file_findings;
|
|
1883
|
-
for (const f of corrFindings) {
|
|
1884
|
-
const key = `${f.title}::${f.file || ''}`;
|
|
1885
|
-
if (!seen.has(key)) {
|
|
1886
|
-
seen.add(key);
|
|
1887
|
-
mergedFindings.push(f);
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
console.log(` ${c.yellow}${corrFindings.length} cross-file issues found${c.reset}`);
|
|
1891
|
-
// Re-merge into report
|
|
1892
|
-
const newRisk = Math.min(100, mergedFindings.reduce((s, f) => s + (sevWeights[f.severity] || 0), 0));
|
|
1893
|
-
report.findings = mergedFindings;
|
|
1894
|
-
report.findings_count = mergedFindings.length;
|
|
1895
|
-
report.risk_score = newRisk;
|
|
1896
|
-
report.result = newRisk === 0 ? 'safe' : newRisk <= 20 ? 'caution' : 'unsafe';
|
|
1897
|
-
totalInput += corrResult.meta?.input_tokens || 0;
|
|
1898
|
-
totalOutput += corrResult.meta?.output_tokens || 0;
|
|
1899
|
-
providerMeta = { ...providerMeta, input_tokens: totalInput || null, output_tokens: totalOutput || null };
|
|
1659
|
+
} else if (activeLlm.type === 'gemini') {
|
|
1660
|
+
// Google Gemini API (unique format)
|
|
1661
|
+
const res = await fetch(`${activeLlm.url}/${activeLlm.model}:generateContent?key=${llmApiKey}`, {
|
|
1662
|
+
method: 'POST',
|
|
1663
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1664
|
+
body: JSON.stringify({
|
|
1665
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
1666
|
+
contents: [{ role: 'user', parts: [{ text: userMessage }] }],
|
|
1667
|
+
generationConfig: { maxOutputTokens: 8192 },
|
|
1668
|
+
}),
|
|
1669
|
+
signal: AbortSignal.timeout(120_000),
|
|
1670
|
+
});
|
|
1671
|
+
data = await res.json();
|
|
1672
|
+
if (data.error) {
|
|
1673
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1674
|
+
const friendly = formatApiError(data.error, activeLlm.provider, res.status);
|
|
1675
|
+
if (friendly) {
|
|
1676
|
+
console.log(` ${c.red}${friendly.text}${c.reset}`);
|
|
1677
|
+
console.log(` ${c.dim}${friendly.hint}${c.reset}`);
|
|
1900
1678
|
} else {
|
|
1901
|
-
console.log(`
|
|
1679
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1902
1680
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1681
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1682
|
+
return null;
|
|
1905
1683
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1684
|
+
_lastLlmText = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
1685
|
+
report = extractJSON(_lastLlmText);
|
|
1686
|
+
if (report) {
|
|
1687
|
+
report.audit_model = data.modelVersion || activeLlm.model;
|
|
1688
|
+
report.audit_provider = activeLlm.provider;
|
|
1689
|
+
if (data.usageMetadata) {
|
|
1690
|
+
report.input_tokens = data.usageMetadata.promptTokenCount;
|
|
1691
|
+
report.output_tokens = data.usageMetadata.candidatesTokenCount;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
} else {
|
|
1695
|
+
// OpenAI-compatible API (OpenAI, Mistral, Groq, OpenRouter, etc.)
|
|
1696
|
+
const headers = {
|
|
1697
|
+
'Authorization': `Bearer ${llmApiKey}`,
|
|
1698
|
+
'Content-Type': 'application/json',
|
|
1699
|
+
};
|
|
1700
|
+
// OpenRouter requires additional headers
|
|
1701
|
+
if (activeLlm.provider === 'openrouter') {
|
|
1702
|
+
headers['HTTP-Referer'] = 'https://agentaudit.dev';
|
|
1703
|
+
headers['X-Title'] = 'AgentAudit CLI';
|
|
1704
|
+
}
|
|
1705
|
+
const res = await fetch(activeLlm.url, {
|
|
1706
|
+
method: 'POST',
|
|
1707
|
+
headers,
|
|
1708
|
+
body: JSON.stringify({
|
|
1709
|
+
model: activeLlm.model,
|
|
1710
|
+
max_tokens: 8192,
|
|
1711
|
+
messages: [
|
|
1712
|
+
{ role: 'system', content: systemPrompt },
|
|
1713
|
+
{ role: 'user', content: userMessage },
|
|
1714
|
+
],
|
|
1715
|
+
}),
|
|
1716
|
+
signal: AbortSignal.timeout(120_000),
|
|
1717
|
+
});
|
|
1718
|
+
data = await res.json();
|
|
1719
|
+
if (data.error) {
|
|
1720
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1721
|
+
const friendly = formatApiError(data.error, activeLlm.provider, res.status);
|
|
1722
|
+
if (friendly) {
|
|
1723
|
+
console.log(` ${c.red}${friendly.text}${c.reset}`);
|
|
1724
|
+
console.log(` ${c.dim}${friendly.hint}${c.reset}`);
|
|
1725
|
+
} else {
|
|
1726
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1727
|
+
}
|
|
1728
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1729
|
+
return null;
|
|
1925
1730
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1731
|
+
_lastLlmText = data.choices?.[0]?.message?.content || '';
|
|
1732
|
+
report = extractJSON(_lastLlmText);
|
|
1733
|
+
if (report) {
|
|
1734
|
+
report.audit_model = data.model || activeLlm.model;
|
|
1735
|
+
report.audit_provider = activeLlm.provider;
|
|
1736
|
+
if (data.id) report.provider_msg_id = data.id;
|
|
1737
|
+
if (data.system_fingerprint) report.provider_fingerprint = data.system_fingerprint;
|
|
1738
|
+
if (data.usage) {
|
|
1739
|
+
report.input_tokens = data.usage.prompt_tokens;
|
|
1740
|
+
report.output_tokens = data.usage.completion_tokens;
|
|
1741
|
+
}
|
|
1929
1742
|
}
|
|
1930
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1931
|
-
return null;
|
|
1932
1743
|
}
|
|
1933
1744
|
|
|
1934
|
-
report = result.report;
|
|
1935
|
-
providerMeta = result.meta;
|
|
1936
|
-
_lastLlmText = result.rawText || '';
|
|
1937
1745
|
console.log(` ${c.green}done${c.reset} ${c.dim}(${elapsed(start)})${c.reset}`);
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1748
|
+
if (err.name === 'TimeoutError' || err.message?.includes('timeout')) {
|
|
1749
|
+
console.log(` ${c.red}Request timed out (120s)${c.reset}`);
|
|
1750
|
+
console.log(` ${c.dim}The provider took too long to respond. Try again or use a faster model${c.reset}`);
|
|
1751
|
+
} else if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.message?.includes('fetch failed')) {
|
|
1752
|
+
console.log(` ${c.red}Network error: could not reach ${activeProvider}${c.reset}`);
|
|
1753
|
+
console.log(` ${c.dim}Check your internet connection or provider status${c.reset}`);
|
|
1754
|
+
} else {
|
|
1755
|
+
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
1756
|
+
}
|
|
1757
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1758
|
+
return null;
|
|
1938
1759
|
}
|
|
1939
1760
|
|
|
1940
1761
|
// Cleanup repo
|
|
1941
1762
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1942
1763
|
|
|
1943
1764
|
if (!report) {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
console.log(` ${c.dim}This model may not support structured JSON output or the prompt was too large.${c.reset}`);
|
|
1948
|
-
console.log(` ${c.dim}Try a different model: --model anthropic/claude-sonnet-4 or --model openai/gpt-4o${c.reset}`);
|
|
1949
|
-
} else {
|
|
1950
|
-
console.log(` ${c.red}✖ Could not parse LLM response as JSON${c.reset}`);
|
|
1951
|
-
console.log(` ${c.dim}The model returned ${rawLen} chars but not valid JSON. Try a stronger model.${c.reset}`);
|
|
1952
|
-
if (!process.argv.includes('--debug')) {
|
|
1953
|
-
console.log(` ${c.dim}Hint: run with --debug to see the raw LLM response${c.reset}`);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
if (process.argv.includes('--debug') && rawLen > 0) {
|
|
1765
|
+
console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
|
|
1766
|
+
console.log(` ${c.dim}Hint: run with --debug to see the raw LLM response${c.reset}`);
|
|
1767
|
+
if (process.argv.includes('--debug')) {
|
|
1957
1768
|
console.log(` ${c.dim}--- Raw LLM response (first 2000 chars) ---${c.reset}`);
|
|
1958
|
-
console.log(_lastLlmText.slice(0, 2000));
|
|
1769
|
+
console.log((typeof _lastLlmText === 'string' ? _lastLlmText : '(empty)').slice(0, 2000));
|
|
1959
1770
|
console.log(` ${c.dim}--- end ---${c.reset}`);
|
|
1960
1771
|
}
|
|
1961
1772
|
return null;
|
|
@@ -1963,19 +1774,8 @@ async function auditRepo(url) {
|
|
|
1963
1774
|
|
|
1964
1775
|
// Display results
|
|
1965
1776
|
console.log();
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
const recalcRisk = report.findings && report.findings.length > 0
|
|
1969
|
-
? Math.min(100, report.findings.reduce((s, f) => s + (_sevW[f.severity] || 0), 0))
|
|
1970
|
-
: 0;
|
|
1971
|
-
report.risk_score = recalcRisk;
|
|
1972
|
-
report.result = recalcRisk === 0 ? 'safe' : recalcRisk <= 20 ? 'caution' : 'unsafe';
|
|
1973
|
-
const riskScore = recalcRisk;
|
|
1974
|
-
const trustScore = 100 - riskScore;
|
|
1975
|
-
const trustColor = trustScore >= 70 ? c.green : trustScore >= 40 ? c.yellow : c.red;
|
|
1976
|
-
const trustLabel = trustScore >= 70 ? 'SAFE' : trustScore >= 40 ? 'CAUTION' : 'UNSAFE';
|
|
1977
|
-
console.log(` ${trustColor}${c.bold}${trustLabel}${c.reset} ${trustColor}Trust Score: ${trustScore}/100${c.reset} ${c.dim}(Risk: ${riskScore}/100)${c.reset}`);
|
|
1978
|
-
console.log(` ${c.dim}Model: ${resolvedProvider.id}/${actualModel} Duration: ${elapsed(start)}${c.reset}`);
|
|
1777
|
+
const riskScore = report.risk_score || 0;
|
|
1778
|
+
console.log(` ${riskBadge(riskScore)} Risk ${riskScore}/100 ${c.bold}${report.result || 'unknown'}${c.reset}`);
|
|
1979
1779
|
console.log();
|
|
1980
1780
|
|
|
1981
1781
|
if (report.findings && report.findings.length > 0) {
|
|
@@ -1995,17 +1795,6 @@ async function auditRepo(url) {
|
|
|
1995
1795
|
|
|
1996
1796
|
// Upload to registry
|
|
1997
1797
|
const creds = loadCredentials();
|
|
1998
|
-
const finalModel = providerMeta.reported_model || actualModel;
|
|
1999
|
-
const finalProvider = resolvedProvider.id;
|
|
2000
|
-
if (!finalModel || finalModel === 'unknown') {
|
|
2001
|
-
console.log(` ${c.yellow}⚠ Model not detected — report will not include model attestation.${c.reset}`);
|
|
2002
|
-
console.log(` ${c.dim} This usually means the LLM API did not return model info.${c.reset}`);
|
|
2003
|
-
console.log(` ${c.dim} Try: agentaudit config set model <model-name>${c.reset}`);
|
|
2004
|
-
if (process.argv.includes('--debug')) {
|
|
2005
|
-
console.log(` ${c.dim} providerMeta: ${JSON.stringify(providerMeta)}${c.reset}`);
|
|
2006
|
-
console.log(` ${c.dim} actualModel: ${actualModel}, resolvedProvider: ${resolvedProvider.id}${c.reset}`);
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
1798
|
if (creds) {
|
|
2010
1799
|
process.stdout.write(` Uploading report to registry...`);
|
|
2011
1800
|
try {
|
|
@@ -2015,83 +1804,18 @@ async function auditRepo(url) {
|
|
|
2015
1804
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
2016
1805
|
'Content-Type': 'application/json',
|
|
2017
1806
|
},
|
|
2018
|
-
body: JSON.stringify(
|
|
2019
|
-
...report,
|
|
2020
|
-
commit_sha: report.commit_sha || repoCommitSha || undefined,
|
|
2021
|
-
package_version: report.package_version || repoPackageVersion || undefined,
|
|
2022
|
-
audit_model: finalModel !== 'unknown' ? finalModel : undefined,
|
|
2023
|
-
audit_provider: finalProvider,
|
|
2024
|
-
provider_msg_id: providerMeta.provider_msg_id || undefined,
|
|
2025
|
-
provider_fingerprint: providerMeta.provider_fingerprint || undefined,
|
|
2026
|
-
input_tokens: providerMeta.input_tokens || undefined,
|
|
2027
|
-
output_tokens: providerMeta.output_tokens || undefined,
|
|
2028
|
-
audit_duration_ms: Date.now() - start,
|
|
2029
|
-
files_scanned: files.length,
|
|
2030
|
-
}),
|
|
1807
|
+
body: JSON.stringify(report),
|
|
2031
1808
|
signal: AbortSignal.timeout(15_000),
|
|
2032
1809
|
});
|
|
2033
1810
|
if (res.ok) {
|
|
2034
1811
|
const data = await res.json();
|
|
2035
|
-
const reportSlug = data?.skill_slug || data?.slug || slug;
|
|
2036
1812
|
console.log(` ${c.green}done${c.reset}`);
|
|
2037
|
-
console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${
|
|
2038
|
-
// Show API warnings
|
|
2039
|
-
if (data?.warnings?.length) {
|
|
2040
|
-
for (const w of data.warnings) {
|
|
2041
|
-
console.log(` ${c.yellow}⚠ ${w}${c.reset}`);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
// Refresh stats cache in background
|
|
2045
|
-
if (creds.agent_name) refreshStatsCache(creds.agent_name).catch(() => {});
|
|
2046
|
-
// Fetch registry consensus after upload
|
|
2047
|
-
try {
|
|
2048
|
-
const regData = await checkRegistry(reportSlug);
|
|
2049
|
-
if (regData) {
|
|
2050
|
-
const regRisk = regData.latest_risk_score ?? regData.risk_score ?? 0;
|
|
2051
|
-
const regTrust = regData.trust_score ?? (100 - regRisk);
|
|
2052
|
-
const totalReports = regData.total_reports ?? 1;
|
|
2053
|
-
const uniqueAgents = regData.unique_agents ?? 1;
|
|
2054
|
-
const confidence = regData.confidence ?? 'unverified';
|
|
2055
|
-
const totalFindings = regData.total_findings ?? 0;
|
|
2056
|
-
const hasOfficial = regData.has_official_audit;
|
|
2057
|
-
|
|
2058
|
-
// Show registry consensus if different from own report or multiple reports exist
|
|
2059
|
-
if (totalReports > 1 || regRisk !== report.risk_score) {
|
|
2060
|
-
console.log();
|
|
2061
|
-
console.log(` ${c.bold}Registry Consensus${c.reset}`);
|
|
2062
|
-
const trustColor = regTrust >= 70 ? c.green : regTrust >= 40 ? c.yellow : c.red;
|
|
2063
|
-
const trustLabel = regTrust >= 70 ? 'SAFE' : regTrust >= 40 ? 'CAUTION' : 'UNSAFE';
|
|
2064
|
-
console.log(` ${trustColor}${c.bold}${trustLabel}${c.reset} Trust Score: ${trustColor}${regTrust}/100${c.reset} ${c.dim}(across ${totalReports} reports from ${uniqueAgents} auditor${uniqueAgents !== 1 ? 's' : ''})${c.reset}`);
|
|
2065
|
-
if (totalFindings > 0) {
|
|
2066
|
-
console.log(` ${c.dim}Total findings across all reports: ${totalFindings}${c.reset}`);
|
|
2067
|
-
}
|
|
2068
|
-
const confDisplay = {
|
|
2069
|
-
consensus: { icon: '🟢', label: 'Consensus Certified', color: c.green },
|
|
2070
|
-
verified: { icon: '🟢', label: 'Verified', color: c.green },
|
|
2071
|
-
low: { icon: '🟡', label: 'Low Confidence', color: c.yellow },
|
|
2072
|
-
unverified: { icon: '🔴', label: 'Unverified', color: c.yellow },
|
|
2073
|
-
}[confidence] || { icon: '⚪', label: confidence, color: c.dim };
|
|
2074
|
-
console.log(` ${confDisplay.icon} ${confDisplay.color}${confDisplay.label}${c.reset}`);
|
|
2075
|
-
if (hasOfficial) console.log(` ${c.green}✔ Officially audited${c.reset}`);
|
|
2076
|
-
|
|
2077
|
-
// Warn if own report disagrees with consensus
|
|
2078
|
-
const ownRisk = report.risk_score || 0;
|
|
2079
|
-
if (Math.abs(ownRisk - regRisk) > 10) {
|
|
2080
|
-
console.log();
|
|
2081
|
-
console.log(` ${c.yellow}⚠ Your audit (risk ${ownRisk}) differs from registry consensus (risk ${regRisk}).${c.reset}`);
|
|
2082
|
-
console.log(` ${c.dim} This may indicate model-specific blind spots or a changing codebase.${c.reset}`);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
} catch {}
|
|
1813
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
|
|
2087
1814
|
} else {
|
|
2088
|
-
const errBody = await res.text().catch(() => '');
|
|
2089
1815
|
console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
|
|
2090
|
-
if (process.argv.includes('--debug')) console.log(` ${c.dim}Response: ${errBody.slice(0, 500)}${c.reset}`);
|
|
2091
1816
|
}
|
|
2092
1817
|
} catch (err) {
|
|
2093
1818
|
console.log(` ${c.yellow}failed${c.reset}`);
|
|
2094
|
-
if (process.argv.includes('--debug')) console.log(` ${c.dim}Error: ${err.message}${c.reset}`);
|
|
2095
1819
|
}
|
|
2096
1820
|
} else {
|
|
2097
1821
|
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to upload reports to the registry${c.reset}`);
|
|
@@ -2103,89 +1827,28 @@ async function auditRepo(url) {
|
|
|
2103
1827
|
|
|
2104
1828
|
// ── Check command ───────────────────────────────────────
|
|
2105
1829
|
|
|
2106
|
-
async function checkPackage(name
|
|
2107
|
-
// Derive slug from URL for registry lookup (URLs won't match as-is)
|
|
2108
|
-
const slug = (name.includes('github.com') || name.includes('://')) ? slugFromUrl(name) : name.toLowerCase();
|
|
1830
|
+
async function checkPackage(name) {
|
|
2109
1831
|
if (!jsonMode) {
|
|
2110
1832
|
console.log(`${icons.info} Looking up ${c.bold}${name}${c.reset} in registry...`);
|
|
2111
1833
|
console.log();
|
|
2112
1834
|
}
|
|
2113
1835
|
|
|
2114
|
-
const data = await checkRegistry(
|
|
1836
|
+
const data = await checkRegistry(name);
|
|
2115
1837
|
if (!data) {
|
|
2116
1838
|
if (!jsonMode) {
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
console.log(` ${c.yellow}Not found in registry.${c.reset}`);
|
|
2120
|
-
console.log(` ${c.dim}Starting audit for ${name}...${c.reset}`);
|
|
2121
|
-
console.log();
|
|
2122
|
-
return await auditRepo(name);
|
|
2123
|
-
}
|
|
2124
|
-
console.log(` ${c.yellow}✖ Not found${c.reset} — "${name}" hasn't been audited yet.`);
|
|
2125
|
-
console.log();
|
|
2126
|
-
console.log(` ${c.dim}Next steps:${c.reset}`);
|
|
2127
|
-
console.log(` ${c.cyan}agentaudit check <repo-url>${c.reset} ${c.dim}Auto-lookup + audit if not found${c.reset}`);
|
|
2128
|
-
console.log(` ${c.cyan}agentaudit audit <repo-url>${c.reset} ${c.dim}Deep LLM audit${c.reset}`);
|
|
2129
|
-
console.log(` ${c.cyan}agentaudit scan <repo-url>${c.reset} ${c.dim}Quick static check (no API key)${c.reset}`);
|
|
1839
|
+
console.log(` ${c.yellow}Not found${c.reset} — package "${name}" hasn't been audited yet.`);
|
|
1840
|
+
console.log(` ${c.dim}Run: agentaudit audit <repo-url> for a deep LLM audit${c.reset}`);
|
|
2130
1841
|
}
|
|
2131
1842
|
return null;
|
|
2132
1843
|
}
|
|
2133
1844
|
|
|
2134
1845
|
if (!jsonMode) {
|
|
2135
1846
|
const riskScore = data.risk_score ?? data.latest_risk_score ?? 0;
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
// Package name + verdict
|
|
2141
|
-
console.log(` ${c.bold}${data.display_name || name}${c.reset} ${riskBadge(riskScore)}`);
|
|
2142
|
-
if (data.description) console.log(` ${c.dim}${data.description}${c.reset}`);
|
|
2143
|
-
console.log();
|
|
2144
|
-
|
|
2145
|
-
// Trust Score (the main metric)
|
|
2146
|
-
const trustColor = trustScore >= 70 ? c.green : trustScore >= 40 ? c.yellow : c.red;
|
|
2147
|
-
const trustLabel = trustScore >= 70 ? 'SAFE' : trustScore >= 40 ? 'CAUTION' : 'UNSAFE';
|
|
2148
|
-
console.log(` ${trustColor}${c.bold}${trustLabel}${c.reset} ${trustColor}Trust Score: ${trustScore}/100${c.reset} ${c.dim}(Risk: ${riskScore}/100)${c.reset}`);
|
|
2149
|
-
|
|
2150
|
-
// Findings summary
|
|
2151
|
-
if (totalFindings > 0) {
|
|
2152
|
-
const maxSev = data.latest_max_severity;
|
|
2153
|
-
const sevStr = maxSev ? `max severity: ${severityColor(maxSev)}${maxSev}${c.reset}` : '';
|
|
2154
|
-
console.log(` ${c.dim}Findings: ${totalFindings}${sevStr ? ` (${sevStr}${c.dim})` : ''}${c.reset}`);
|
|
2155
|
-
} else {
|
|
2156
|
-
console.log(` ${c.dim}Findings: 0 (clean)${c.reset}`);
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// Consensus / Confidence
|
|
2160
|
-
const uniqueAgents = data.unique_agents ?? 0;
|
|
2161
|
-
const confidence = data.confidence ?? 'unverified';
|
|
2162
|
-
const confidenceDisplay = {
|
|
2163
|
-
consensus: { icon: '🟢', label: 'Consensus Certified', color: c.green, desc: `${totalReports} reports from ${uniqueAgents} independent auditors agree` },
|
|
2164
|
-
verified: { icon: '🟢', label: 'Verified', color: c.green, desc: `${totalReports} reports from ${uniqueAgents} auditors` },
|
|
2165
|
-
low: { icon: '🟡', label: 'Low Confidence', color: c.yellow, desc: `${totalReports} reports but ${uniqueAgents <= 1 ? 'only 1 auditor' : `only ${uniqueAgents} auditors`}` },
|
|
2166
|
-
unverified: { icon: '🔴', label: 'Unverified', color: c.yellow, desc: 'Single audit, no independent confirmation' },
|
|
2167
|
-
}[confidence] || { icon: '⚪', label: confidence, color: c.dim, desc: '' };
|
|
2168
|
-
console.log(` ${confidenceDisplay.icon} ${confidenceDisplay.color}${confidenceDisplay.label}${c.reset} ${c.dim}${confidenceDisplay.desc}${c.reset}`);
|
|
2169
|
-
|
|
2170
|
-
// Audit info
|
|
2171
|
-
console.log(` ${c.dim}Reports: ${totalReports} | Auditors: ${uniqueAgents} | Last: ${data.last_audited_at ? new Date(data.last_audited_at).toLocaleDateString() : 'unknown'}${c.reset}`);
|
|
1847
|
+
console.log(` ${c.bold}${name}${c.reset} ${riskBadge(riskScore)}`);
|
|
1848
|
+
console.log(` ${c.dim}Risk Score: ${riskScore}/100${c.reset}`);
|
|
1849
|
+
if (data.source_url) console.log(` ${c.dim}Source: ${data.source_url}${c.reset}`);
|
|
1850
|
+
console.log(` ${c.dim}Registry: ${REGISTRY_URL}/skills/${name}${c.reset}`);
|
|
2172
1851
|
if (data.has_official_audit) console.log(` ${c.green}✔ Officially audited${c.reset}`);
|
|
2173
|
-
|
|
2174
|
-
// Recommendation
|
|
2175
|
-
if (confidence === 'unverified' && trustScore >= 70) {
|
|
2176
|
-
console.log();
|
|
2177
|
-
console.log(` ${c.yellow}⚠ Score looks good but only 1 audit exists.${c.reset}`);
|
|
2178
|
-
console.log(` ${c.dim} Consider running your own audit: agentaudit audit ${data.source_url || name}${c.reset}`);
|
|
2179
|
-
} else if (confidence === 'low') {
|
|
2180
|
-
console.log();
|
|
2181
|
-
console.log(` ${c.yellow}⚠ Limited independent verification.${c.reset}`);
|
|
2182
|
-
console.log(` ${c.dim} More auditors needed for consensus. Run: agentaudit audit ${data.source_url || name}${c.reset}`);
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
// Links
|
|
2186
|
-
console.log();
|
|
2187
|
-
if (data.source_url) console.log(` ${c.dim}Source: ${data.source_url}${c.reset}`);
|
|
2188
|
-
console.log(` ${c.dim}Registry: ${REGISTRY_URL}/skills/${encodeURIComponent(name)}${c.reset}`);
|
|
2189
1852
|
console.log();
|
|
2190
1853
|
}
|
|
2191
1854
|
return data;
|
|
@@ -2207,40 +1870,12 @@ async function main() {
|
|
|
2207
1870
|
quietMode = rawArgs.includes('--quiet') || rawArgs.includes('-q');
|
|
2208
1871
|
// --no-color already handled at top level for `c` object
|
|
2209
1872
|
|
|
2210
|
-
//
|
|
2211
|
-
const modelFlagIdx = rawArgs.findIndex(a => a === '--model');
|
|
2212
|
-
const modelFlagEq = rawArgs.find(a => a.startsWith('--model='));
|
|
2213
|
-
modelOverride = modelFlagEq?.split('=')[1]
|
|
2214
|
-
|| (modelFlagIdx >= 0 ? rawArgs[modelFlagIdx + 1] : null)
|
|
2215
|
-
|| process.env.AGENTAUDIT_MODEL
|
|
2216
|
-
|| loadConfig()?.preferred_model
|
|
2217
|
-
|| null;
|
|
2218
|
-
globalModelOverride = modelOverride;
|
|
2219
|
-
|
|
2220
|
-
// --timeout flag: --timeout=<seconds> or --timeout <seconds>
|
|
2221
|
-
const timeoutFlagIdx = rawArgs.findIndex(a => a === '--timeout');
|
|
2222
|
-
const timeoutFlagEq = rawArgs.find(a => a.startsWith('--timeout='));
|
|
2223
|
-
const timeoutVal = timeoutFlagEq?.split('=')[1]
|
|
2224
|
-
|| (timeoutFlagIdx >= 0 ? rawArgs[timeoutFlagIdx + 1] : null)
|
|
2225
|
-
|| process.env.AGENTAUDIT_TIMEOUT
|
|
2226
|
-
|| null;
|
|
2227
|
-
if (timeoutVal) {
|
|
2228
|
-
const secs = parseInt(timeoutVal, 10);
|
|
2229
|
-
if (secs > 0) llmTimeoutMs = secs * 1000;
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
// Strip global flags from args
|
|
1873
|
+
// Strip global flags from args (including --model <value>)
|
|
2233
1874
|
const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color']);
|
|
2234
1875
|
let args = rawArgs.filter(a => !globalFlags.has(a));
|
|
2235
|
-
//
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
if (a === '--model') { arr[i + 1] = '__skip__'; return false; }
|
|
2239
|
-
if (a.startsWith('--timeout=')) return false;
|
|
2240
|
-
if (a === '--timeout') { arr[i + 1] = '__skip__'; return false; }
|
|
2241
|
-
if (a === '__skip__') return false;
|
|
2242
|
-
return true;
|
|
2243
|
-
});
|
|
1876
|
+
// Remove --model <value> pair
|
|
1877
|
+
const modelIdx = args.indexOf('--model');
|
|
1878
|
+
if (modelIdx !== -1) args.splice(modelIdx, 2);
|
|
2244
1879
|
|
|
2245
1880
|
if (args[0] === '-v' || args[0] === '--version') {
|
|
2246
1881
|
console.log(`agentaudit ${getVersion()}`);
|
|
@@ -2248,46 +1883,48 @@ async function main() {
|
|
|
2248
1883
|
}
|
|
2249
1884
|
|
|
2250
1885
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
2251
|
-
|
|
2252
|
-
console.log(` ${c.bold}
|
|
2253
|
-
console.log(
|
|
1886
|
+
banner();
|
|
1887
|
+
console.log(` ${c.bold}Commands:${c.reset}`);
|
|
1888
|
+
console.log();
|
|
1889
|
+
console.log(` ${c.cyan}agentaudit${c.reset} Discover MCP servers (same as discover)`);
|
|
1890
|
+
console.log(` ${c.cyan}agentaudit discover${c.reset} Find MCP servers in your AI editors (Cursor, Claude, VS Code, Windsurf)`);
|
|
1891
|
+
console.log(` ${c.cyan}agentaudit discover --quick${c.reset} Discover + auto-scan all servers`);
|
|
1892
|
+
console.log(` ${c.cyan}agentaudit discover --deep${c.reset} Discover + select servers to deep-audit`);
|
|
1893
|
+
console.log(` ${c.cyan}agentaudit scan${c.reset} <url> [url...] Quick static scan (regex, local)`);
|
|
1894
|
+
console.log(` ${c.cyan}agentaudit scan${c.reset} <url> ${c.dim}--deep${c.reset} Deep audit (same as audit)`);
|
|
1895
|
+
console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
|
|
1896
|
+
console.log(` ${c.cyan}agentaudit lookup${c.reset} <name> Look up package in registry`);
|
|
1897
|
+
console.log(` ${c.cyan}agentaudit model${c.reset} [name|reset] View or set default LLM model`);
|
|
1898
|
+
console.log(` ${c.cyan}agentaudit setup${c.reset} Register + configure API key`);
|
|
2254
1899
|
console.log();
|
|
2255
|
-
console.log(` ${c.bold}
|
|
2256
|
-
console.log(` ${c.
|
|
2257
|
-
console.log(` ${c.
|
|
2258
|
-
console.log(` ${c.
|
|
1900
|
+
console.log(` ${c.bold}Global flags:${c.reset}`);
|
|
1901
|
+
console.log(` ${c.dim}--json Output JSON to stdout (machine-readable)${c.reset}`);
|
|
1902
|
+
console.log(` ${c.dim}--quiet Suppress banner and tree visualization${c.reset}`);
|
|
1903
|
+
console.log(` ${c.dim}--no-color Disable ANSI colors (also: NO_COLOR env)${c.reset}`);
|
|
1904
|
+
console.log(` ${c.dim}--model <name> Override LLM model (e.g. qwen/qwen3.5-coder-32b)${c.reset}`);
|
|
2259
1905
|
console.log();
|
|
2260
|
-
console.log(` ${c.bold}
|
|
2261
|
-
console.log(` ${c.
|
|
2262
|
-
console.log(` ${c.
|
|
1906
|
+
console.log(` ${c.bold}Quick Scan${c.reset} vs ${c.bold}Deep Audit${c.reset}:`);
|
|
1907
|
+
console.log(` ${c.dim}scan = fast regex-based static analysis (~2s)${c.reset}`);
|
|
1908
|
+
console.log(` ${c.dim}audit = deep LLM analysis with 3-pass methodology (~30s)${c.reset}`);
|
|
2263
1909
|
console.log();
|
|
2264
|
-
console.log(` ${c.bold}
|
|
2265
|
-
console.log(` ${c.
|
|
2266
|
-
console.log(` ${c.cyan}setup${c.reset} Register & configure`);
|
|
2267
|
-
console.log(` ${c.cyan}models${c.reset} List available LLM models`);
|
|
2268
|
-
console.log(` ${c.cyan}config set${c.reset} <key> <value> Set default provider/options`);
|
|
1910
|
+
console.log(` ${c.bold}Exit codes:${c.reset}`);
|
|
1911
|
+
console.log(` ${c.dim}0 = clean / success 1 = findings detected 2 = error${c.reset}`);
|
|
2269
1912
|
console.log();
|
|
2270
|
-
console.log(` ${c.bold}
|
|
2271
|
-
console.log(`
|
|
2272
|
-
console.log(`
|
|
2273
|
-
console.log(`
|
|
2274
|
-
console.log(`
|
|
2275
|
-
console.log(`
|
|
2276
|
-
console.log(` ${c.dim}--export Export audit payload to markdown${c.reset}`);
|
|
2277
|
-
console.log(` ${c.dim}--debug Show raw LLM response on errors${c.reset}`);
|
|
1913
|
+
console.log(` ${c.bold}Examples:${c.reset}`);
|
|
1914
|
+
console.log(` agentaudit`);
|
|
1915
|
+
console.log(` agentaudit discover --quick`);
|
|
1916
|
+
console.log(` agentaudit scan https://github.com/owner/repo`);
|
|
1917
|
+
console.log(` agentaudit audit https://github.com/owner/repo`);
|
|
1918
|
+
console.log(` agentaudit lookup fastmcp --json`);
|
|
2278
1919
|
console.log();
|
|
2279
|
-
console.log(` ${c.bold}
|
|
2280
|
-
console.log(` ${c.dim}
|
|
2281
|
-
console.log(` ${c.dim}
|
|
2282
|
-
console.log(` ${c.dim}
|
|
2283
|
-
console.log(` ${c.dim}$${c.reset} agentaudit status`);
|
|
1920
|
+
console.log(` ${c.bold}For deep audits,${c.reset} set any LLM API key:`);
|
|
1921
|
+
console.log(` ${c.dim}ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, DEEPSEEK_API_KEY,${c.reset}`);
|
|
1922
|
+
console.log(` ${c.dim}MISTRAL_API_KEY, XAI_API_KEY, GROQ_API_KEY, TOGETHER_API_KEY,${c.reset}`);
|
|
1923
|
+
console.log(` ${c.dim}FIREWORKS_API_KEY, CEREBRAS_API_KEY, ZAI_API_KEY, or OPENROUTER_API_KEY${c.reset}`);
|
|
2284
1924
|
console.log();
|
|
2285
|
-
console.log(` ${c.bold}
|
|
2286
|
-
console.log(` ${c.dim}
|
|
2287
|
-
console.log(` ${c.dim}
|
|
2288
|
-
console.log(` ${c.dim}Or persist: agentaudit config set provider openai${c.reset}`);
|
|
2289
|
-
console.log(` ${c.dim} agentaudit config set model gpt-4o-mini${c.reset}`);
|
|
2290
|
-
console.log(` ${c.dim}Run ${c.cyan}agentaudit status${c.dim} to check configuration.${c.reset}`);
|
|
1925
|
+
console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed):${c.reset}`);
|
|
1926
|
+
console.log(` ${c.dim}Add to your MCP config:${c.reset}`);
|
|
1927
|
+
console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
|
|
2291
1928
|
console.log();
|
|
2292
1929
|
process.exitCode = 0; return;
|
|
2293
1930
|
}
|
|
@@ -2296,312 +1933,166 @@ async function main() {
|
|
|
2296
1933
|
const command = args.length === 0 ? 'discover' : args[0];
|
|
2297
1934
|
const targets = args.slice(1);
|
|
2298
1935
|
|
|
2299
|
-
|
|
1936
|
+
banner();
|
|
2300
1937
|
|
|
2301
1938
|
if (command === 'setup') {
|
|
2302
1939
|
await setupCommand();
|
|
2303
1940
|
return;
|
|
2304
1941
|
}
|
|
2305
|
-
|
|
2306
|
-
if (command === 'status') {
|
|
2307
|
-
console.log(` ${c.bold}LLM Providers:${c.reset}`);
|
|
2308
|
-
console.log();
|
|
2309
|
-
const keys = {
|
|
2310
|
-
anthropicKey: process.env.ANTHROPIC_API_KEY,
|
|
2311
|
-
openaiKey: process.env.OPENAI_API_KEY,
|
|
2312
|
-
openrouterKey: process.env.OPENROUTER_API_KEY,
|
|
2313
|
-
};
|
|
2314
|
-
const ollamaHost = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
2315
|
-
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
2316
|
-
const customUrl = process.env.LLM_API_URL;
|
|
2317
|
-
|
|
2318
|
-
const checks = [
|
|
2319
|
-
{ 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' }] }) },
|
|
2320
|
-
{ 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' }] }) },
|
|
2321
|
-
{ 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' }] }) },
|
|
2322
|
-
{ name: 'Ollama', env: 'OLLAMA_MODEL', key: ollamaModel, testUrl: `${ollamaHost}/api/tags`, testHeaders: () => ({}), testBody: null },
|
|
2323
|
-
{ 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 },
|
|
2324
|
-
];
|
|
2325
1942
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
});
|
|
2340
|
-
if (res.ok || res.status === 200 || res.status === 201) {
|
|
2341
|
-
process.stdout.write(`\r ${c.green}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.green}valid ✓${c.reset} \n`);
|
|
2342
|
-
} else {
|
|
2343
|
-
const body = await res.json().catch(() => ({}));
|
|
2344
|
-
const rawMsg = body?.error?.message || body?.message || `HTTP ${res.status}`;
|
|
2345
|
-
// Detect specific error types for clearer messages
|
|
2346
|
-
const lcMsg = rawMsg.toLowerCase();
|
|
2347
|
-
let errMsg = rawMsg;
|
|
2348
|
-
let hint = '';
|
|
2349
|
-
if (lcMsg.includes('credit') || lcMsg.includes('balance') || lcMsg.includes('quota') || lcMsg.includes('billing') || lcMsg.includes('exceeded') || lcMsg.includes('insufficient')) {
|
|
2350
|
-
errMsg = 'no credits';
|
|
2351
|
-
if (p.name === 'Anthropic') hint = `\n ${c.dim}└─ Add credits: console.anthropic.com/settings/plans${c.reset}`;
|
|
2352
|
-
else if (p.name === 'OpenAI') hint = `\n ${c.dim}└─ Check usage: platform.openai.com/usage${c.reset}`;
|
|
2353
|
-
else if (p.name === 'OpenRouter') hint = `\n ${c.dim}└─ Check balance: openrouter.ai/credits${c.reset}`;
|
|
2354
|
-
} else if (res.status === 401 || lcMsg.includes('invalid') || lcMsg.includes('unauthorized') || lcMsg.includes('authentication')) {
|
|
2355
|
-
errMsg = 'invalid key';
|
|
2356
|
-
if (p.name === 'Anthropic') hint = `\n ${c.dim}└─ Check key: console.anthropic.com/settings/keys${c.reset}`;
|
|
2357
|
-
else if (p.name === 'OpenAI') hint = `\n ${c.dim}└─ Check key: platform.openai.com/api-keys${c.reset}`;
|
|
2358
|
-
} else if (res.status === 429) {
|
|
2359
|
-
errMsg = 'rate limited';
|
|
2360
|
-
hint = `\n ${c.dim}└─ Try again in a moment${c.reset}`;
|
|
2361
|
-
}
|
|
2362
|
-
process.stdout.write(`\r ${c.red}●${c.reset} ${p.name.padEnd(12)} ${c.dim}${masked}${c.reset} ${c.red}✖ ${errMsg}${c.reset}${hint} \n`);
|
|
1943
|
+
if (command === 'model') {
|
|
1944
|
+
const newModel = targets.filter(t => !t.startsWith('--'))[0];
|
|
1945
|
+
const current = loadLlmConfig()?.llm_model;
|
|
1946
|
+
|
|
1947
|
+
// Direct set: agentaudit model <name>
|
|
1948
|
+
if (newModel === 'reset' || newModel === 'clear') {
|
|
1949
|
+
for (const f of [USER_CRED_FILE, SKILL_CRED_FILE]) {
|
|
1950
|
+
if (fs.existsSync(f)) {
|
|
1951
|
+
try {
|
|
1952
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
1953
|
+
delete data.llm_model;
|
|
1954
|
+
fs.writeFileSync(f, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
1955
|
+
} catch {}
|
|
2363
1956
|
}
|
|
2364
|
-
} catch (e) {
|
|
2365
|
-
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`);
|
|
2366
1957
|
}
|
|
1958
|
+
console.log(` ${icons.safe} Model reset to provider default`);
|
|
1959
|
+
return;
|
|
2367
1960
|
}
|
|
2368
1961
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
console.log(` ${c.dim}Override: --provider=<name> --model=<name>${c.reset}`);
|
|
2375
|
-
console.log(` ${c.dim}Set default: agentaudit config set provider <name>${c.reset}`);
|
|
2376
|
-
console.log(` ${c.dim} agentaudit config set model <name>${c.reset}`);
|
|
2377
|
-
} else {
|
|
2378
|
-
console.log(` ${c.yellow}⚠ No working LLM provider.${c.reset} Deep audits require one.`);
|
|
2379
|
-
console.log(` ${c.dim}Set a key: export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
2380
|
-
console.log(` ${c.dim}Or scan without LLM: agentaudit scan <url>${c.reset}`);
|
|
1962
|
+
if (newModel) {
|
|
1963
|
+
saveLlmConfig(newModel);
|
|
1964
|
+
console.log(` ${icons.safe} Model set to ${c.bold}${newModel}${c.reset}`);
|
|
1965
|
+
if (current) console.log(` ${c.dim}Was: ${current}${c.reset}`);
|
|
1966
|
+
return;
|
|
2381
1967
|
}
|
|
2382
1968
|
|
|
2383
|
-
//
|
|
1969
|
+
// No argument — interactive menu
|
|
1970
|
+
const activeLlm = LLM_PROVIDERS.find(p => process.env[p.key]);
|
|
1971
|
+
console.log(` ${c.bold}LLM Model Configuration${c.reset}`);
|
|
2384
1972
|
console.log();
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
if (creds?.api_key) {
|
|
2388
|
-
const masked = creds.api_key.substring(0, 8) + '...' + creds.api_key.substring(creds.api_key.length - 4);
|
|
2389
|
-
console.log(` ${c.green}●${c.reset} AgentAudit ${c.dim}${masked}${c.reset} ${c.dim}(${creds.agent_name || 'unknown'})${c.reset}`);
|
|
2390
|
-
// Fetch agent stats from leaderboard
|
|
2391
|
-
try {
|
|
2392
|
-
const lbRes = await fetch(`${REGISTRY_URL}/api/leaderboard`, { signal: AbortSignal.timeout(5000) });
|
|
2393
|
-
if (lbRes.ok) {
|
|
2394
|
-
const agents = await lbRes.json();
|
|
2395
|
-
const myName = creds.agent_name?.toLowerCase();
|
|
2396
|
-
const idx = Array.isArray(agents) ? agents.findIndex((a) => (a.agent_name || '').toLowerCase() === myName) : -1;
|
|
2397
|
-
if (idx >= 0) {
|
|
2398
|
-
const me = agents[idx];
|
|
2399
|
-
const pts = me.total_points || 0;
|
|
2400
|
-
const reports = me.total_reports || 0;
|
|
2401
|
-
const rank = idx + 1;
|
|
2402
|
-
const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : ' ';
|
|
2403
|
-
console.log();
|
|
2404
|
-
console.log(` ${c.bold}Your Stats:${c.reset}`);
|
|
2405
|
-
console.log(` ${medal} Rank #${rank} of ${agents.length} ${c.dim}│${c.reset} ${c.cyan}${pts}${c.reset} points ${c.dim}│${c.reset} ${reports} reports`);
|
|
2406
|
-
if (me.is_official) console.log(` ${c.green}✔ Official Auditor${c.reset}`);
|
|
2407
|
-
// Update stats cache
|
|
2408
|
-
saveStatsCache({ rank, total: agents.length, pts, reports, official: !!me.is_official });
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
} catch {}
|
|
1973
|
+
if (current) {
|
|
1974
|
+
console.log(` Current: ${c.bold}${c.cyan}${current}${c.reset}`);
|
|
2412
1975
|
} else {
|
|
2413
|
-
console.log(`
|
|
1976
|
+
console.log(` Current: ${c.dim}(provider default${activeLlm ? `: ${activeLlm.model}` : ''})${c.reset}`);
|
|
1977
|
+
}
|
|
1978
|
+
if (activeLlm) {
|
|
1979
|
+
console.log(` Provider: ${c.dim}${activeLlm.name} (${activeLlm.key})${c.reset}`);
|
|
2414
1980
|
}
|
|
2415
1981
|
console.log();
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
if (openrouterKey || targets.includes('--all')) {
|
|
2454
|
-
process.stdout.write(` ${c.dim}Fetching models...${c.reset}`);
|
|
2455
|
-
try {
|
|
2456
|
-
const res = await fetch('https://openrouter.ai/api/v1/models', {
|
|
2457
|
-
headers: openrouterKey ? { 'Authorization': `Bearer ${openrouterKey}` } : {},
|
|
2458
|
-
signal: AbortSignal.timeout(10_000),
|
|
2459
|
-
});
|
|
2460
|
-
const data = await res.json();
|
|
2461
|
-
const models = (data.data || [])
|
|
2462
|
-
.filter(m => m.id && !m.id.includes(':free') && !m.id.includes('/extended'))
|
|
2463
|
-
.sort((a, b) => (a.id || '').localeCompare(b.id || ''));
|
|
2464
|
-
|
|
2465
|
-
// Group by provider prefix
|
|
2466
|
-
const groups = {};
|
|
2467
|
-
for (const m of models) {
|
|
2468
|
-
const [prefix] = m.id.split('/');
|
|
2469
|
-
if (!groups[prefix]) groups[prefix] = [];
|
|
2470
|
-
groups[prefix].push(m);
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
// Show popular ones first
|
|
2474
|
-
const popular = ['anthropic', 'openai', 'google', 'meta-llama', 'mistralai', 'deepseek'];
|
|
2475
|
-
const shown = new Set();
|
|
2476
|
-
process.stdout.write(`\r ${c.green}${models.length} models available${c.reset} \n`);
|
|
2477
|
-
console.log();
|
|
2478
|
-
|
|
2479
|
-
for (const prefix of popular) {
|
|
2480
|
-
if (!groups[prefix]) continue;
|
|
2481
|
-
shown.add(prefix);
|
|
2482
|
-
console.log(` ${c.bold}${prefix}${c.reset}`);
|
|
2483
|
-
for (const m of groups[prefix].slice(0, 5)) {
|
|
2484
|
-
console.log(` ${c.dim}${m.id}${c.reset}`);
|
|
2485
|
-
}
|
|
2486
|
-
if (groups[prefix].length > 5) {
|
|
2487
|
-
console.log(` ${c.dim}... and ${groups[prefix].length - 5} more${c.reset}`);
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
const otherCount = Object.keys(groups).filter(k => !shown.has(k)).length;
|
|
2492
|
-
if (otherCount > 0) {
|
|
2493
|
-
console.log();
|
|
2494
|
-
console.log(` ${c.dim}+ ${otherCount} more providers. Use --model=<provider/model>${c.reset}`);
|
|
2495
|
-
console.log(` ${c.dim}Full list: https://openrouter.ai/models${c.reset}`);
|
|
1982
|
+
|
|
1983
|
+
// Build menu items — deduplicate providers, show popular models
|
|
1984
|
+
const modelChoices = [
|
|
1985
|
+
{ label: 'claude-sonnet-4-20250514', sublabel: 'Anthropic — fast + smart', value: 'claude-sonnet-4-20250514' },
|
|
1986
|
+
{ label: 'claude-opus-4-20250514', sublabel: 'Anthropic — most capable', value: 'claude-opus-4-20250514' },
|
|
1987
|
+
{ label: 'gpt-4o', sublabel: 'OpenAI — fast multimodal', value: 'gpt-4o' },
|
|
1988
|
+
{ label: 'gpt-4.1', sublabel: 'OpenAI — latest', value: 'gpt-4.1' },
|
|
1989
|
+
{ label: 'gemini-2.5-flash', sublabel: 'Google — fast + cheap', value: 'gemini-2.5-flash' },
|
|
1990
|
+
{ label: 'gemini-2.5-pro', sublabel: 'Google — most capable', value: 'gemini-2.5-pro' },
|
|
1991
|
+
{ label: 'deepseek-chat', sublabel: 'DeepSeek — cost-effective', value: 'deepseek-chat' },
|
|
1992
|
+
{ label: 'qwen/qwen3-coder', sublabel: 'OpenRouter — code specialist', value: 'qwen/qwen3-coder' },
|
|
1993
|
+
{ label: 'mistral-large-latest', sublabel: 'Mistral — EU-hosted', value: 'mistral-large-latest' },
|
|
1994
|
+
{ label: 'grok-3', sublabel: 'xAI — real-time knowledge', value: 'grok-3' },
|
|
1995
|
+
{ label: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', sublabel: 'Together AI — open source', value: 'meta-llama/Llama-3.3-70B-Instruct-Turbo' },
|
|
1996
|
+
{ label: 'llama-3.3-70b-versatile', sublabel: 'Groq — ultra-fast inference', value: 'llama-3.3-70b-versatile' },
|
|
1997
|
+
{ label: '(reset to provider default)', sublabel: '', value: '__reset__' },
|
|
1998
|
+
{ label: '(enter custom model name)', sublabel: '', value: '__custom__' },
|
|
1999
|
+
];
|
|
2000
|
+
|
|
2001
|
+
const selected = await singleSelect(modelChoices, {
|
|
2002
|
+
title: 'Choose default model',
|
|
2003
|
+
hint: '↑↓=move Enter=select Esc=cancel',
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
if (selected === null) {
|
|
2007
|
+
console.log(` ${c.dim}Cancelled${c.reset}`);
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (selected === '__reset__') {
|
|
2012
|
+
for (const f of [USER_CRED_FILE, SKILL_CRED_FILE]) {
|
|
2013
|
+
if (fs.existsSync(f)) {
|
|
2014
|
+
try {
|
|
2015
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
2016
|
+
delete data.llm_model;
|
|
2017
|
+
fs.writeFileSync(f, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
2018
|
+
} catch {}
|
|
2496
2019
|
}
|
|
2497
|
-
} catch (e) {
|
|
2498
|
-
process.stdout.write(`\r ${c.red}Failed to fetch: ${e.message}${c.reset} \n`);
|
|
2499
2020
|
}
|
|
2500
|
-
|
|
2501
|
-
console.log(`
|
|
2502
|
-
|
|
2503
|
-
console.log(` ${c.dim}Or browse: https://openrouter.ai/models${c.reset}`);
|
|
2021
|
+
console.log();
|
|
2022
|
+
console.log(` ${icons.safe} Model reset to provider default`);
|
|
2023
|
+
return;
|
|
2504
2024
|
}
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
try {
|
|
2513
|
-
const res = await fetch(`${ollamaHost}/api/tags`, { signal: AbortSignal.timeout(5_000) });
|
|
2514
|
-
const data = await res.json();
|
|
2515
|
-
for (const m of (data.models || []).slice(0, 10)) {
|
|
2516
|
-
console.log(` ${c.dim}${m.name}${c.reset}`);
|
|
2517
|
-
}
|
|
2518
|
-
} catch {
|
|
2519
|
-
console.log(` ${c.dim}(Ollama not running at ${ollamaHost})${c.reset}`);
|
|
2025
|
+
|
|
2026
|
+
if (selected === '__custom__') {
|
|
2027
|
+
console.log();
|
|
2028
|
+
const custom = await askQuestion(` Model name: `);
|
|
2029
|
+
if (!custom) {
|
|
2030
|
+
console.log(` ${c.dim}Cancelled${c.reset}`);
|
|
2031
|
+
return;
|
|
2520
2032
|
}
|
|
2521
|
-
|
|
2522
|
-
console.log(`
|
|
2033
|
+
saveLlmConfig(custom);
|
|
2034
|
+
console.log(` ${icons.safe} Model set to ${c.bold}${custom}${c.reset}`);
|
|
2035
|
+
if (current) console.log(` ${c.dim}Was: ${current}${c.reset}`);
|
|
2036
|
+
return;
|
|
2523
2037
|
}
|
|
2038
|
+
|
|
2039
|
+
saveLlmConfig(selected);
|
|
2524
2040
|
console.log();
|
|
2525
|
-
|
|
2526
|
-
console.log(` ${c.
|
|
2527
|
-
console.log(
|
|
2528
|
-
console.log(`
|
|
2529
|
-
console.log(` ${c.dim}Or env: AGENTAUDIT_MODEL=<name>${c.reset}`);
|
|
2041
|
+
console.log(` ${icons.safe} Model set to ${c.bold}${selected}${c.reset}`);
|
|
2042
|
+
if (current) console.log(` ${c.dim}Was: ${current}${c.reset}`);
|
|
2043
|
+
console.log();
|
|
2044
|
+
console.log(` ${c.dim}Tip: agentaudit model <name> to set directly, or agentaudit model reset${c.reset}`);
|
|
2530
2045
|
return;
|
|
2531
2046
|
}
|
|
2532
2047
|
|
|
2533
|
-
if (command === '
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
const val = targets[2].toLowerCase();
|
|
2538
|
-
if (!validProviders.includes(val)) {
|
|
2539
|
-
console.log(` ${c.red}✖ Unknown provider: ${val}${c.reset}`);
|
|
2540
|
-
console.log(` ${c.dim}Valid: anthropic, openai, openrouter, ollama, custom${c.reset}`);
|
|
2541
|
-
process.exitCode = 2; return;
|
|
2542
|
-
}
|
|
2543
|
-
saveConfig({ preferred_provider: val });
|
|
2544
|
-
console.log(` ${c.green}✔${c.reset} Default provider set to: ${c.bold}${val}${c.reset}`);
|
|
2545
|
-
console.log(` ${c.dim}Override per-command: --provider=<name>${c.reset}`);
|
|
2546
|
-
console.log(` ${c.dim}Or env: AGENTAUDIT_PROVIDER=<name>${c.reset}`);
|
|
2547
|
-
} else if (subCmd === 'set' && targets[1] === 'model' && targets[2]) {
|
|
2548
|
-
const val = targets[2];
|
|
2549
|
-
saveConfig({ preferred_model: val });
|
|
2550
|
-
console.log(` ${c.green}✔${c.reset} Default model set to: ${c.bold}${val}${c.reset}`);
|
|
2551
|
-
console.log(` ${c.dim}Override per-command: --model=<name>${c.reset}`);
|
|
2552
|
-
console.log(` ${c.dim}Or env: AGENTAUDIT_MODEL=<name>${c.reset}`);
|
|
2553
|
-
} else if (subCmd === 'get' || !subCmd) {
|
|
2554
|
-
const cfg = loadConfig();
|
|
2555
|
-
console.log(` ${c.bold}Config:${c.reset} ${USER_CONFIG_FILE}`);
|
|
2556
|
-
if (Object.keys(cfg).length === 0) {
|
|
2557
|
-
console.log(` ${c.dim}(empty — using defaults)${c.reset}`);
|
|
2558
|
-
} else {
|
|
2559
|
-
for (const [k, v] of Object.entries(cfg)) {
|
|
2560
|
-
console.log(` ${c.dim}${k}:${c.reset} ${v}`);
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
} else if (subCmd === 'reset') {
|
|
2564
|
-
try { fs.unlinkSync(USER_CONFIG_FILE); } catch {}
|
|
2565
|
-
console.log(` ${c.green}✔${c.reset} Config reset to defaults.`);
|
|
2566
|
-
} else {
|
|
2567
|
-
console.log(` ${c.red}✖ Unknown config command${c.reset}`);
|
|
2568
|
-
console.log(` ${c.dim}Usage: agentaudit config set provider <name>${c.reset}`);
|
|
2569
|
-
console.log(` ${c.dim} agentaudit config get${c.reset}`);
|
|
2570
|
-
console.log(` ${c.dim} agentaudit config reset${c.reset}`);
|
|
2571
|
-
}
|
|
2048
|
+
if (command === 'discover') {
|
|
2049
|
+
const scanFlag = targets.includes('--quick') || targets.includes('--scan') || targets.includes('-s');
|
|
2050
|
+
const auditFlag = targets.includes('--deep') || targets.includes('--audit') || targets.includes('-a');
|
|
2051
|
+
await discoverCommand({ scan: scanFlag, audit: auditFlag });
|
|
2572
2052
|
return;
|
|
2573
2053
|
}
|
|
2574
2054
|
|
|
2575
2055
|
if (command === 'lookup' || command === 'check') {
|
|
2576
2056
|
const names = targets.filter(t => !t.startsWith('--'));
|
|
2577
2057
|
if (names.length === 0) {
|
|
2578
|
-
console.log(` ${c.red}
|
|
2579
|
-
console.log(` ${c.dim}Usage: agentaudit check <name|url>${c.reset}`);
|
|
2058
|
+
console.log(` ${c.red}Error: package name required${c.reset}`);
|
|
2580
2059
|
process.exitCode = 2;
|
|
2581
2060
|
return;
|
|
2582
2061
|
}
|
|
2583
2062
|
const results = [];
|
|
2584
|
-
const allowAutoAudit = command === 'check'; // only 'check' auto-audits, 'lookup' never does
|
|
2585
2063
|
for (const t of names) {
|
|
2586
|
-
const data = await checkPackage(t
|
|
2064
|
+
const data = await checkPackage(t);
|
|
2587
2065
|
results.push(data);
|
|
2588
2066
|
}
|
|
2589
2067
|
if (jsonMode) {
|
|
2590
2068
|
console.log(JSON.stringify(results.length === 1 ? (results[0] || { error: 'not_found' }) : results, null, 2));
|
|
2591
2069
|
}
|
|
2592
2070
|
process.exitCode = 0; return;
|
|
2071
|
+
return;
|
|
2593
2072
|
}
|
|
2594
2073
|
|
|
2595
2074
|
if (command === 'scan') {
|
|
2075
|
+
const deepFlag = targets.includes('--deep');
|
|
2596
2076
|
const urls = targets.filter(t => !t.startsWith('--'));
|
|
2597
2077
|
if (urls.length === 0) {
|
|
2598
|
-
console.log(` ${c.red}
|
|
2599
|
-
console.log(`
|
|
2600
|
-
console.log(`
|
|
2078
|
+
console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
|
|
2079
|
+
console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit discover${c.dim} to find & check locally installed MCP servers${c.reset}`);
|
|
2080
|
+
console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit audit <url>${c.dim} for a deep LLM-powered audit${c.reset}`);
|
|
2601
2081
|
process.exitCode = 2;
|
|
2602
2082
|
return;
|
|
2603
2083
|
}
|
|
2604
2084
|
|
|
2085
|
+
// --deep redirects to audit flow
|
|
2086
|
+
if (deepFlag) {
|
|
2087
|
+
let hasFindings = false;
|
|
2088
|
+
for (const url of urls) {
|
|
2089
|
+
const report = await auditRepo(url);
|
|
2090
|
+
if (report?.findings?.length > 0) hasFindings = true;
|
|
2091
|
+
}
|
|
2092
|
+
process.exitCode = hasFindings ? 1 : 0;
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2605
2096
|
const results = [];
|
|
2606
2097
|
let hadErrors = false;
|
|
2607
2098
|
for (const url of urls) {
|
|
@@ -2630,40 +2121,30 @@ async function main() {
|
|
|
2630
2121
|
}
|
|
2631
2122
|
|
|
2632
2123
|
if (hadErrors && results.length === 0) { process.exitCode = 2; return; }
|
|
2633
|
-
|
|
2124
|
+
const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2125
|
+
process.exitCode = totalFindings > 0 ? 1 : 0;
|
|
2634
2126
|
return;
|
|
2635
2127
|
}
|
|
2636
2128
|
|
|
2637
2129
|
if (command === 'audit') {
|
|
2638
2130
|
const urls = targets.filter(t => !t.startsWith('--'));
|
|
2639
2131
|
if (urls.length === 0) {
|
|
2640
|
-
console.log(` ${c.red}
|
|
2641
|
-
console.log(` ${c.dim}Usage: agentaudit audit <url>${c.reset}`);
|
|
2132
|
+
console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
|
|
2642
2133
|
process.exitCode = 2;
|
|
2643
2134
|
return;
|
|
2644
2135
|
}
|
|
2645
2136
|
|
|
2646
|
-
let
|
|
2137
|
+
let hasFindings = false;
|
|
2647
2138
|
for (const url of urls) {
|
|
2648
2139
|
const report = await auditRepo(url);
|
|
2649
|
-
if (
|
|
2140
|
+
if (report?.findings?.length > 0) hasFindings = true;
|
|
2650
2141
|
}
|
|
2651
|
-
process.exitCode =
|
|
2142
|
+
process.exitCode = hasFindings ? 1 : 0;
|
|
2652
2143
|
return;
|
|
2653
2144
|
}
|
|
2654
2145
|
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
const suggestion = knownCommands
|
|
2658
|
-
.map(cmd => ({ cmd, dist: levenshtein(command, cmd) }))
|
|
2659
|
-
.filter(x => x.dist <= 3)
|
|
2660
|
-
.sort((a, b) => a.dist - b.dist)[0];
|
|
2661
|
-
|
|
2662
|
-
console.log(` ${c.red}✖ Unknown command: ${command}${c.reset}`);
|
|
2663
|
-
if (suggestion) {
|
|
2664
|
-
console.log(` ${c.dim}Did you mean: ${c.cyan}agentaudit ${suggestion.cmd}${c.reset}${c.dim}?${c.reset}`);
|
|
2665
|
-
}
|
|
2666
|
-
console.log(` ${c.dim}Run ${c.cyan}agentaudit --help${c.dim} for usage${c.reset}`);
|
|
2146
|
+
console.log(` ${c.red}Unknown command: ${command}${c.reset}`);
|
|
2147
|
+
console.log(` ${c.dim}Run agentaudit --help for usage${c.reset}`);
|
|
2667
2148
|
process.exitCode = 2;
|
|
2668
2149
|
}
|
|
2669
2150
|
|