agentaudit 3.9.48 → 3.10.1
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 +741 -1121
- 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, --no-upload
|
|
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,75 @@ 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
|
+
];
|
|
51
|
+
|
|
52
|
+
// ── Provider-specific model choices (for interactive menu) ──
|
|
53
|
+
const PROVIDER_MODELS = {
|
|
54
|
+
anthropic: [
|
|
55
|
+
{ label: 'claude-sonnet-4-20250514', sublabel: 'fast + smart (default)', value: 'claude-sonnet-4-20250514' },
|
|
56
|
+
{ label: 'claude-opus-4-20250514', sublabel: 'most capable', value: 'claude-opus-4-20250514' },
|
|
57
|
+
],
|
|
58
|
+
openai: [
|
|
59
|
+
{ label: 'gpt-4o', sublabel: 'fast multimodal (default)', value: 'gpt-4o' },
|
|
60
|
+
{ label: 'gpt-4.1', sublabel: 'latest', value: 'gpt-4.1' },
|
|
61
|
+
],
|
|
62
|
+
google: [
|
|
63
|
+
{ label: 'gemini-2.5-flash', sublabel: 'fast + cheap (default)', value: 'gemini-2.5-flash' },
|
|
64
|
+
{ label: 'gemini-2.5-pro', sublabel: 'most capable', value: 'gemini-2.5-pro' },
|
|
65
|
+
],
|
|
66
|
+
deepseek: [
|
|
67
|
+
{ label: 'deepseek-chat', sublabel: 'cost-effective (default)', value: 'deepseek-chat' },
|
|
68
|
+
],
|
|
69
|
+
mistral: [
|
|
70
|
+
{ label: 'mistral-large-latest', sublabel: 'EU-hosted (default)', value: 'mistral-large-latest' },
|
|
71
|
+
],
|
|
72
|
+
groq: [
|
|
73
|
+
{ label: 'llama-3.3-70b-versatile', sublabel: 'ultra-fast (default)', value: 'llama-3.3-70b-versatile' },
|
|
74
|
+
],
|
|
75
|
+
xai: [
|
|
76
|
+
{ label: 'grok-3', sublabel: 'real-time knowledge (default)', value: 'grok-3' },
|
|
77
|
+
],
|
|
78
|
+
together: [
|
|
79
|
+
{ label: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', sublabel: 'open source (default)', value: 'meta-llama/Llama-3.3-70B-Instruct-Turbo' },
|
|
80
|
+
],
|
|
81
|
+
fireworks: [
|
|
82
|
+
{ label: 'accounts/fireworks/models/llama-v3p3-70b-instruct', sublabel: 'open source (default)', value: 'accounts/fireworks/models/llama-v3p3-70b-instruct' },
|
|
83
|
+
],
|
|
84
|
+
cerebras: [
|
|
85
|
+
{ label: 'llama-3.3-70b', sublabel: 'fast inference (default)', value: 'llama-3.3-70b' },
|
|
86
|
+
],
|
|
87
|
+
zhipu: [
|
|
88
|
+
{ label: 'glm-4.7', sublabel: 'Chinese language (default)', value: 'glm-4.7' },
|
|
89
|
+
],
|
|
90
|
+
openrouter: [
|
|
91
|
+
{ label: 'anthropic/claude-sonnet-4', sublabel: 'default', value: 'anthropic/claude-sonnet-4' },
|
|
92
|
+
{ label: 'qwen/qwen3-coder', sublabel: 'code specialist', value: 'qwen/qwen3-coder' },
|
|
93
|
+
{ label: 'meta-llama/Llama-3.3-70B', sublabel: 'open source', value: 'meta-llama/Llama-3.3-70B' },
|
|
94
|
+
],
|
|
95
|
+
};
|
|
79
96
|
|
|
80
97
|
// ── ANSI Colors (respects NO_COLOR and --no-color) ───────
|
|
81
98
|
|
|
@@ -137,55 +154,70 @@ function loadCredentials() {
|
|
|
137
154
|
return null;
|
|
138
155
|
}
|
|
139
156
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
157
|
+
function loadLlmConfig() {
|
|
158
|
+
for (const f of [SKILL_CRED_FILE, USER_CRED_FILE]) {
|
|
159
|
+
if (fs.existsSync(f)) {
|
|
160
|
+
try {
|
|
161
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
162
|
+
if (data.llm_model || data.preferred_provider) {
|
|
163
|
+
return { llm_model: data.llm_model || null, preferred_provider: data.preferred_provider || null };
|
|
164
|
+
}
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function saveLlmConfig(model, provider) {
|
|
172
|
+
// Merge into existing credentials
|
|
173
|
+
let existing = {};
|
|
174
|
+
if (fs.existsSync(USER_CRED_FILE)) {
|
|
175
|
+
try { existing = JSON.parse(fs.readFileSync(USER_CRED_FILE, 'utf8')); } catch {}
|
|
176
|
+
}
|
|
177
|
+
if (model !== undefined) existing.llm_model = model;
|
|
178
|
+
if (provider !== undefined) existing.preferred_provider = provider;
|
|
179
|
+
const json = JSON.stringify(existing, null, 2);
|
|
142
180
|
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
143
181
|
fs.writeFileSync(USER_CRED_FILE, json, { mode: 0o600 });
|
|
144
182
|
try {
|
|
183
|
+
let skillExisting = {};
|
|
184
|
+
if (fs.existsSync(SKILL_CRED_FILE)) {
|
|
185
|
+
try { skillExisting = JSON.parse(fs.readFileSync(SKILL_CRED_FILE, 'utf8')); } catch {}
|
|
186
|
+
}
|
|
187
|
+
if (model !== undefined) skillExisting.llm_model = model;
|
|
188
|
+
if (provider !== undefined) skillExisting.preferred_provider = provider;
|
|
145
189
|
fs.mkdirSync(path.dirname(SKILL_CRED_FILE), { recursive: true });
|
|
146
|
-
fs.writeFileSync(SKILL_CRED_FILE,
|
|
190
|
+
fs.writeFileSync(SKILL_CRED_FILE, JSON.stringify(skillExisting, null, 2), { mode: 0o600 });
|
|
147
191
|
} catch {}
|
|
148
192
|
}
|
|
149
193
|
|
|
150
|
-
|
|
151
|
-
const
|
|
194
|
+
function resolveProvider() {
|
|
195
|
+
const config = loadLlmConfig();
|
|
196
|
+
const preferred = config?.preferred_provider;
|
|
152
197
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
198
|
+
if (preferred) {
|
|
199
|
+
// Find provider by name, check if any of their keys is set
|
|
200
|
+
const match = LLM_PROVIDERS.find(p => p.provider === preferred && process.env[p.key]);
|
|
201
|
+
if (match) return match;
|
|
202
|
+
// Key missing for preferred provider — warn + fallback
|
|
203
|
+
const providerInfo = LLM_PROVIDERS.find(p => p.provider === preferred);
|
|
204
|
+
if (providerInfo && !quietMode) {
|
|
205
|
+
console.log(` ${c.yellow}Preferred provider "${providerInfo.name}" missing key (${providerInfo.key}), falling back...${c.reset}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
160
208
|
|
|
161
|
-
|
|
162
|
-
|
|
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; }
|
|
209
|
+
// Fallback: first match wins
|
|
210
|
+
return LLM_PROVIDERS.find(p => process.env[p.key]) || null;
|
|
173
211
|
}
|
|
174
212
|
|
|
175
|
-
function
|
|
213
|
+
function saveCredentials(data) {
|
|
214
|
+
const json = JSON.stringify(data, null, 2);
|
|
215
|
+
fs.mkdirSync(USER_CRED_DIR, { recursive: true });
|
|
216
|
+
fs.writeFileSync(USER_CRED_FILE, json, { mode: 0o600 });
|
|
176
217
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
218
|
+
fs.mkdirSync(path.dirname(SKILL_CRED_FILE), { recursive: true });
|
|
219
|
+
fs.writeFileSync(SKILL_CRED_FILE, json, { mode: 0o600 });
|
|
180
220
|
} 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
221
|
}
|
|
190
222
|
|
|
191
223
|
function askQuestion(question) {
|
|
@@ -298,6 +330,77 @@ function multiSelect(items, { title = 'Select items', hint = 'Space=toggle ↑
|
|
|
298
330
|
});
|
|
299
331
|
}
|
|
300
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Interactive single-select in terminal. No dependencies.
|
|
335
|
+
* items: [{ label, sublabel?, value }]
|
|
336
|
+
* Returns: selected value (or null if cancelled)
|
|
337
|
+
*/
|
|
338
|
+
function singleSelect(items, { title = 'Select', hint = '↑↓=move Enter=select Esc=cancel' } = {}) {
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
if (!process.stdin.isTTY) {
|
|
341
|
+
resolve(items[0]?.value || null);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let cursor = 0;
|
|
346
|
+
|
|
347
|
+
const render = () => {
|
|
348
|
+
process.stdout.write(`\x1b[${items.length + 3}A\x1b[J`);
|
|
349
|
+
draw();
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const draw = () => {
|
|
353
|
+
console.log(` ${c.bold}${title}${c.reset}`);
|
|
354
|
+
console.log(` ${c.dim}${hint}${c.reset}`);
|
|
355
|
+
console.log();
|
|
356
|
+
for (let i = 0; i < items.length; i++) {
|
|
357
|
+
const item = items[i];
|
|
358
|
+
const isCursor = i === cursor;
|
|
359
|
+
const pointer = isCursor ? `${c.cyan}❯${c.reset}` : ' ';
|
|
360
|
+
const label = isCursor ? `${c.bold}${c.cyan}${item.label}${c.reset}` : item.label;
|
|
361
|
+
const sub = item.sublabel ? ` ${c.dim}${item.sublabel}${c.reset}` : '';
|
|
362
|
+
console.log(` ${pointer} ${label}${sub}`);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
draw();
|
|
367
|
+
|
|
368
|
+
process.stdin.setRawMode(true);
|
|
369
|
+
process.stdin.resume();
|
|
370
|
+
process.stdin.setEncoding('utf8');
|
|
371
|
+
|
|
372
|
+
const onData = (key) => {
|
|
373
|
+
if (key === '\x03' || key === '\x1b') {
|
|
374
|
+
process.stdin.setRawMode(false);
|
|
375
|
+
process.stdin.pause();
|
|
376
|
+
process.stdin.removeListener('data', onData);
|
|
377
|
+
console.log();
|
|
378
|
+
resolve(null);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (key === '\r' || key === '\n') {
|
|
382
|
+
process.stdin.setRawMode(false);
|
|
383
|
+
process.stdin.pause();
|
|
384
|
+
process.stdin.removeListener('data', onData);
|
|
385
|
+
resolve(items[cursor].value);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
389
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
390
|
+
render();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
394
|
+
cursor = (cursor + 1) % items.length;
|
|
395
|
+
render();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
process.stdin.on('data', onData);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
301
404
|
async function registerAgent(agentName) {
|
|
302
405
|
const res = await fetch(`${REGISTRY_URL}/api/register`, {
|
|
303
406
|
method: 'POST',
|
|
@@ -354,8 +457,6 @@ async function setupCommand() {
|
|
|
354
457
|
console.log(` ${icons.safe} Registered as ${c.bold}${data.agent_name}${c.reset}`);
|
|
355
458
|
console.log(` ${c.dim}Key: ${data.api_key.slice(0, 12)}...${c.reset}`);
|
|
356
459
|
console.log(` ${c.dim}Saved to: ${USER_CRED_FILE}${c.reset}`);
|
|
357
|
-
// Initialize stats cache
|
|
358
|
-
refreshStatsCache(data.agent_name).catch(() => {});
|
|
359
460
|
} catch (err) {
|
|
360
461
|
console.log(` ${c.red}failed${c.reset}`);
|
|
361
462
|
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
@@ -364,6 +465,20 @@ async function setupCommand() {
|
|
|
364
465
|
}
|
|
365
466
|
|
|
366
467
|
console.log();
|
|
468
|
+
|
|
469
|
+
// ── LLM configuration hint ──
|
|
470
|
+
const llmConfig = loadLlmConfig();
|
|
471
|
+
console.log(` ${c.bold}LLM Configuration${c.reset}`);
|
|
472
|
+
if (llmConfig?.llm_model || llmConfig?.preferred_provider) {
|
|
473
|
+
const parts = [];
|
|
474
|
+
if (llmConfig.preferred_provider) parts.push(llmConfig.preferred_provider);
|
|
475
|
+
if (llmConfig.llm_model) parts.push(llmConfig.llm_model);
|
|
476
|
+
console.log(` ${icons.safe} Current: ${c.bold}${parts.join(' → ')}${c.reset}`);
|
|
477
|
+
}
|
|
478
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit model${c.dim} to configure your LLM provider and model.${c.reset}`);
|
|
479
|
+
console.log(` ${c.dim}Deep audits require an LLM API key in your environment.${c.reset}`);
|
|
480
|
+
console.log();
|
|
481
|
+
|
|
367
482
|
console.log(` ${c.bold}Ready!${c.reset} You can now:`);
|
|
368
483
|
console.log(` ${c.dim}•${c.reset} Discover servers: ${c.cyan}agentaudit discover${c.reset}`);
|
|
369
484
|
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 +488,27 @@ async function setupCommand() {
|
|
|
373
488
|
console.log();
|
|
374
489
|
}
|
|
375
490
|
|
|
376
|
-
// ──
|
|
491
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
377
492
|
|
|
378
|
-
function
|
|
379
|
-
|
|
380
|
-
|
|
493
|
+
function validateGitUrl(url) {
|
|
494
|
+
// Reject URLs with shell metacharacters to prevent command injection
|
|
495
|
+
if (/[;&|`$(){}!\n\r]/.test(url)) {
|
|
496
|
+
throw new Error(`Rejected URL with suspicious characters: ${url.slice(0, 80)}`);
|
|
497
|
+
}
|
|
498
|
+
// Must look like a URL (http/https/git/ssh) or a GitHub shorthand
|
|
499
|
+
if (!/^(https?:\/\/|git@|git:\/\/|ssh:\/\/)/.test(url) && !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(url)) {
|
|
500
|
+
throw new Error(`Invalid repository URL: ${url.slice(0, 80)}`);
|
|
381
501
|
}
|
|
382
|
-
process.exitCode = exitCode;
|
|
383
502
|
}
|
|
384
503
|
|
|
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];
|
|
504
|
+
function safeGitClone(url, destPath, timeoutMs = 30_000) {
|
|
505
|
+
validateGitUrl(url);
|
|
506
|
+
execFileSync('git', ['clone', '--depth', '1', url, destPath], {
|
|
507
|
+
timeout: timeoutMs,
|
|
508
|
+
stdio: 'pipe',
|
|
509
|
+
});
|
|
396
510
|
}
|
|
397
511
|
|
|
398
|
-
// ── Helpers ──────────────────────────────────────────────
|
|
399
|
-
|
|
400
512
|
function getVersion() {
|
|
401
513
|
try {
|
|
402
514
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
@@ -404,38 +516,17 @@ function getVersion() {
|
|
|
404
516
|
} catch { return '0.0.0'; }
|
|
405
517
|
}
|
|
406
518
|
|
|
407
|
-
|
|
519
|
+
function banner() {
|
|
408
520
|
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
521
|
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
|
-
}
|
|
522
|
+
console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
|
|
523
|
+
console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
|
|
426
524
|
console.log();
|
|
427
525
|
}
|
|
428
526
|
|
|
429
527
|
function slugFromUrl(url) {
|
|
430
528
|
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
|
-
}
|
|
529
|
+
if (match) return match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
439
530
|
return url.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60);
|
|
440
531
|
}
|
|
441
532
|
|
|
@@ -474,6 +565,46 @@ function severityIcon(sev) {
|
|
|
474
565
|
|
|
475
566
|
// ── File Collection (same logic as MCP server) ──────────
|
|
476
567
|
|
|
568
|
+
function formatApiError(error, provider, statusCode) {
|
|
569
|
+
// Extract error message from various API response formats
|
|
570
|
+
const msg = (typeof error === 'string' ? error : error?.message || error?.error?.message || JSON.stringify(error)).toLowerCase();
|
|
571
|
+
|
|
572
|
+
// Authentication errors
|
|
573
|
+
if (statusCode === 401 || statusCode === 403 || msg.includes('invalid api key') || msg.includes('invalid x-api-key') ||
|
|
574
|
+
msg.includes('incorrect api key') || msg.includes('authentication') || msg.includes('unauthorized') ||
|
|
575
|
+
msg.includes('invalid_api_key') || msg.includes('permission denied')) {
|
|
576
|
+
return { text: 'Invalid or expired API key', hint: `Check your ${provider} API key. Run: echo $${provider}_API_KEY` };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Rate limits / quota
|
|
580
|
+
if (statusCode === 429 || msg.includes('rate limit') || msg.includes('rate_limit') || msg.includes('too many requests') ||
|
|
581
|
+
msg.includes('quota') || msg.includes('insufficient_quota') || msg.includes('billing') ||
|
|
582
|
+
msg.includes('exceeded') || msg.includes('no credits') || msg.includes('credit') ||
|
|
583
|
+
msg.includes('overloaded') || msg.includes('capacity')) {
|
|
584
|
+
return { text: 'Rate limit or quota exceeded', hint: 'Wait a moment and retry, or check your billing/credits at your provider dashboard' };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Model not found
|
|
588
|
+
if (statusCode === 404 || msg.includes('not found') || msg.includes('not a valid model') ||
|
|
589
|
+
msg.includes('model_not_found') || msg.includes('does not exist') || msg.includes('invalid model')) {
|
|
590
|
+
return { text: 'Model not found', hint: `"${msg}" — check model name with: agentaudit model` };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Context length / payload too large
|
|
594
|
+
if (statusCode === 413 || msg.includes('context length') || msg.includes('too long') ||
|
|
595
|
+
msg.includes('maximum') || msg.includes('token limit') || msg.includes('content_too_large')) {
|
|
596
|
+
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' };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Server errors
|
|
600
|
+
if (statusCode >= 500) {
|
|
601
|
+
return { text: `Provider server error (HTTP ${statusCode})`, hint: `${provider} might be experiencing issues. Try again later` };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Fallback
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
477
608
|
function extractJSON(text) {
|
|
478
609
|
// 1. Try parsing the entire text as JSON directly
|
|
479
610
|
try { return JSON.parse(text.trim()); } catch {}
|
|
@@ -526,60 +657,36 @@ function extractJSON(text) {
|
|
|
526
657
|
|
|
527
658
|
const MAX_FILE_SIZE = 50_000;
|
|
528
659
|
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
660
|
const SKIP_DIRS = new Set([
|
|
539
|
-
'node_modules', '.git', '__pycache__', '.venv', 'venv', '
|
|
540
|
-
'
|
|
541
|
-
'
|
|
542
|
-
'.vscode', '.idea',
|
|
661
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
662
|
+
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
663
|
+
'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
|
|
664
|
+
'examples', 'example', 'fixtures', '.github', '.vscode', '.idea',
|
|
665
|
+
'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
|
|
543
666
|
]);
|
|
544
667
|
const SKIP_EXTENSIONS = new Set([
|
|
545
668
|
'.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff',
|
|
546
669
|
'.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.tar', '.gz',
|
|
547
670
|
'.map', '.min.js', '.min.css', '.d.ts', '.pyc', '.pyo', '.so',
|
|
548
671
|
'.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
672
|
]);
|
|
561
673
|
|
|
562
|
-
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
563
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE)
|
|
674
|
+
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
675
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
564
676
|
let entries;
|
|
565
677
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
566
678
|
catch { return collected; }
|
|
567
679
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
568
680
|
for (const entry of entries) {
|
|
681
|
+
if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
|
|
569
682
|
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
570
|
-
if (totalSize.bytes >= MAX_TOTAL_SIZE) { totalSize.truncated = true; totalSize.skippedPaths.push(relPath); continue; }
|
|
571
683
|
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
684
|
if (entry.isDirectory()) {
|
|
575
|
-
|
|
576
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
577
|
-
if (entry.name.startsWith('.') && entry.name !== '.github') continue;
|
|
685
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
578
686
|
collectFiles(fullPath, relPath, collected, totalSize);
|
|
579
687
|
} else {
|
|
580
688
|
const ext = path.extname(entry.name).toLowerCase();
|
|
581
689
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
582
|
-
if (SKIP_FILES.has(entry.name.toLowerCase())) continue;
|
|
583
690
|
try {
|
|
584
691
|
const stat = fs.statSync(fullPath);
|
|
585
692
|
if (stat.size > MAX_FILE_SIZE || stat.size === 0) continue;
|
|
@@ -924,10 +1031,7 @@ async function scanRepo(url) {
|
|
|
924
1031
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
925
1032
|
const repoPath = path.join(tmpDir, 'repo');
|
|
926
1033
|
try {
|
|
927
|
-
|
|
928
|
-
timeout: 30_000,
|
|
929
|
-
stdio: 'pipe',
|
|
930
|
-
});
|
|
1034
|
+
safeGitClone(url, repoPath);
|
|
931
1035
|
} catch (err) {
|
|
932
1036
|
if (!jsonMode) {
|
|
933
1037
|
process.stdout.write(` ${c.red}✖ clone failed${c.reset}\n`);
|
|
@@ -1437,9 +1541,7 @@ async function auditRepo(url) {
|
|
|
1437
1541
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
1438
1542
|
const repoPath = path.join(tmpDir, 'repo');
|
|
1439
1543
|
try {
|
|
1440
|
-
|
|
1441
|
-
timeout: 30_000, stdio: 'pipe',
|
|
1442
|
-
});
|
|
1544
|
+
safeGitClone(url, repoPath);
|
|
1443
1545
|
console.log(` ${c.green}done${c.reset}`);
|
|
1444
1546
|
} catch (err) {
|
|
1445
1547
|
console.log(` ${c.red}failed${c.reset}`);
|
|
@@ -1449,165 +1551,48 @@ async function auditRepo(url) {
|
|
|
1449
1551
|
return null;
|
|
1450
1552
|
}
|
|
1451
1553
|
|
|
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
1554
|
// Step 2: Collect files
|
|
1464
1555
|
process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
|
|
1465
|
-
const
|
|
1466
|
-
const files = collectFiles(repoPath, '', [], _collectMeta);
|
|
1556
|
+
const files = collectFiles(repoPath);
|
|
1467
1557
|
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
|
-
|
|
1503
|
-
if (resolvedProvider) {
|
|
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
1558
|
|
|
1527
|
-
|
|
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
|
|
1559
|
+
// Step 3: Build audit payload
|
|
1532
1560
|
process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
|
|
1533
1561
|
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
1562
|
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
} else {
|
|
1572
|
-
console.log(` ${c.green}done${c.reset}`);
|
|
1563
|
+
let codeBlock = '';
|
|
1564
|
+
for (const file of files) {
|
|
1565
|
+
codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
1573
1566
|
}
|
|
1574
|
-
|
|
1575
|
-
const codeBlock = chunks[0] || '';
|
|
1567
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
1576
1568
|
|
|
1577
1569
|
// Step 4: LLM Analysis
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1570
|
+
// Resolve provider: preferred_provider from config → first match fallback
|
|
1571
|
+
const activeLlm = resolveProvider();
|
|
1572
|
+
const llmApiKey = activeLlm ? process.env[activeLlm.key] : null;
|
|
1573
|
+
const activeProvider = activeLlm ? activeLlm.name : null;
|
|
1574
|
+
|
|
1575
|
+
// Model override: --model flag > AGENTAUDIT_MODEL env > credentials.json > provider default
|
|
1576
|
+
const modelArgIdx = process.argv.indexOf('--model');
|
|
1577
|
+
const modelFlag = modelArgIdx !== -1 ? process.argv[modelArgIdx + 1] : null;
|
|
1578
|
+
const modelEnv = process.env.AGENTAUDIT_MODEL;
|
|
1579
|
+
const modelConfig = loadLlmConfig()?.llm_model;
|
|
1580
|
+
const modelOverride = modelFlag || modelEnv || modelConfig || null;
|
|
1581
|
+
if (activeLlm && modelOverride) {
|
|
1582
|
+
activeLlm.model = modelOverride;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (!activeLlm) {
|
|
1586
|
+
// No LLM API key — compact explanation
|
|
1595
1587
|
console.log();
|
|
1596
|
-
console.log(` ${c.
|
|
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}`);
|
|
1588
|
+
console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
|
|
1599
1589
|
console.log();
|
|
1600
|
-
console.log(` ${c.
|
|
1601
|
-
console.log(` ${c.dim}
|
|
1602
|
-
console.log(` ${c.dim}set OPENAI_API_KEY=sk-...${c.reset}`);
|
|
1590
|
+
console.log(` ${c.bold}Set an API key${c.reset} (e.g. ${c.cyan}export OPENROUTER_API_KEY=sk-or-...${c.reset})`);
|
|
1591
|
+
console.log(` ${c.dim}Run "agentaudit model" to configure provider + model interactively${c.reset}`);
|
|
1603
1592
|
console.log();
|
|
1604
|
-
console.log(` ${c.bold}
|
|
1605
|
-
console.log(` ${c.
|
|
1606
|
-
console.log(` ${c.dim}
|
|
1607
|
-
console.log();
|
|
1608
|
-
console.log(` ${c.bold}Option 3: Use MCP in Claude/Cursor/Windsurf (no API key needed)${c.reset}`);
|
|
1609
|
-
console.log(` ${c.dim}Add AgentAudit as MCP server — your editor's agent runs the audit using its own LLM.${c.reset}`);
|
|
1610
|
-
console.log(` ${c.dim}Config: { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }${c.reset}`);
|
|
1593
|
+
console.log(` ${c.bold}Or export for manual review:${c.reset} ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
|
|
1594
|
+
console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed)${c.reset}`);
|
|
1595
|
+
console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
|
|
1611
1596
|
console.log();
|
|
1612
1597
|
|
|
1613
1598
|
// Check if --export flag
|
|
@@ -1643,319 +1628,181 @@ async function auditRepo(url) {
|
|
|
1643
1628
|
return null;
|
|
1644
1629
|
}
|
|
1645
1630
|
|
|
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
|
-
}
|
|
1631
|
+
// We have an API key — run LLM audit
|
|
1632
|
+
const modelLabel = modelOverride ? `${activeProvider} → ${activeLlm.model}` : activeProvider;
|
|
1633
|
+
process.stdout.write(` ${c.dim}[4/4]${c.reset} Running LLM analysis ${c.dim}(${modelLabel})${c.reset}...`);
|
|
1634
|
+
|
|
1635
|
+
const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
|
|
1636
|
+
const userMessage = [
|
|
1637
|
+
`Audit this package: **${slug}** (${url})`,
|
|
1638
|
+
``,
|
|
1639
|
+
`After analysis, respond with ONLY a valid JSON object. No markdown fences, no explanation, no text before or after. Just the raw JSON:`,
|
|
1640
|
+
`{ "skill_slug": "${slug}", "source_url": "${url}", "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
|
|
1641
|
+
` "risk_score": <0-100>, "result": "<safe|caution|unsafe>", "max_severity": "<none|low|medium|high|critical>",`,
|
|
1642
|
+
` "findings_count": <n>, "findings": [{ "id": "...", "title": "...", "severity": "...", "category": "...",`,
|
|
1643
|
+
` "description": "...", "file": "...", "line": <n>, "remediation": "...", "confidence": "...", "is_by_design": false }] }`,
|
|
1644
|
+
``,
|
|
1645
|
+
`## Source Code`,
|
|
1646
|
+
codeBlock,
|
|
1647
|
+
].join('\n');
|
|
1721
1648
|
|
|
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
1649
|
let report = null;
|
|
1759
|
-
let providerMeta = {};
|
|
1760
1650
|
let _lastLlmText = '';
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1651
|
+
|
|
1652
|
+
try {
|
|
1653
|
+
let data;
|
|
1654
|
+
if (activeLlm.type === 'anthropic') {
|
|
1655
|
+
// Anthropic Messages API (unique format)
|
|
1656
|
+
const res = await fetch(activeLlm.url, {
|
|
1657
|
+
method: 'POST',
|
|
1658
|
+
headers: {
|
|
1659
|
+
'x-api-key': llmApiKey,
|
|
1660
|
+
'anthropic-version': '2023-06-01',
|
|
1661
|
+
'content-type': 'application/json',
|
|
1662
|
+
},
|
|
1663
|
+
body: JSON.stringify({
|
|
1664
|
+
model: activeLlm.model,
|
|
1665
|
+
max_tokens: 8192,
|
|
1666
|
+
system: systemPrompt,
|
|
1667
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
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}`);
|
|
1678
|
+
} else {
|
|
1679
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1780
1680
|
}
|
|
1781
|
-
|
|
1681
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1682
|
+
return null;
|
|
1782
1683
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
if (!baseReport) baseReport = result.report;
|
|
1795
|
-
if (result.report.findings) allFindings.push(...result.report.findings);
|
|
1796
|
-
lastMeta = result.meta;
|
|
1797
|
-
totalInput += result.meta.input_tokens || 0;
|
|
1798
|
-
totalOutput += result.meta.output_tokens || 0;
|
|
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);
|
|
1684
|
+
_lastLlmText = data.content?.[0]?.text || '';
|
|
1685
|
+
report = extractJSON(_lastLlmText);
|
|
1686
|
+
if (report) {
|
|
1687
|
+
report.audit_model = data.model || activeLlm.model;
|
|
1688
|
+
report.audit_provider = activeLlm.provider;
|
|
1689
|
+
if (data.id) report.provider_msg_id = data.id;
|
|
1690
|
+
if (data.usage) {
|
|
1691
|
+
report.input_tokens = data.usage.input_tokens;
|
|
1692
|
+
report.output_tokens = data.usage.output_tokens;
|
|
1693
|
+
}
|
|
1815
1694
|
}
|
|
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 };
|
|
1695
|
+
} else if (activeLlm.type === 'gemini') {
|
|
1696
|
+
// Google Gemini API (unique format)
|
|
1697
|
+
const res = await fetch(`${activeLlm.url}/${activeLlm.model}:generateContent?key=${llmApiKey}`, {
|
|
1698
|
+
method: 'POST',
|
|
1699
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1700
|
+
body: JSON.stringify({
|
|
1701
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
1702
|
+
contents: [{ role: 'user', parts: [{ text: userMessage }] }],
|
|
1703
|
+
generationConfig: { maxOutputTokens: 8192 },
|
|
1704
|
+
}),
|
|
1705
|
+
signal: AbortSignal.timeout(120_000),
|
|
1706
|
+
});
|
|
1707
|
+
data = await res.json();
|
|
1708
|
+
if (data.error) {
|
|
1709
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1710
|
+
const friendly = formatApiError(data.error, activeLlm.provider, res.status);
|
|
1711
|
+
if (friendly) {
|
|
1712
|
+
console.log(` ${c.red}${friendly.text}${c.reset}`);
|
|
1713
|
+
console.log(` ${c.dim}${friendly.hint}${c.reset}`);
|
|
1900
1714
|
} else {
|
|
1901
|
-
console.log(`
|
|
1715
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1902
1716
|
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1717
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1718
|
+
return null;
|
|
1905
1719
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1720
|
+
_lastLlmText = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
1721
|
+
report = extractJSON(_lastLlmText);
|
|
1722
|
+
if (report) {
|
|
1723
|
+
report.audit_model = data.modelVersion || activeLlm.model;
|
|
1724
|
+
report.audit_provider = activeLlm.provider;
|
|
1725
|
+
if (data.usageMetadata) {
|
|
1726
|
+
report.input_tokens = data.usageMetadata.promptTokenCount;
|
|
1727
|
+
report.output_tokens = data.usageMetadata.candidatesTokenCount;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
} else {
|
|
1731
|
+
// OpenAI-compatible API (OpenAI, Mistral, Groq, OpenRouter, etc.)
|
|
1732
|
+
const headers = {
|
|
1733
|
+
'Authorization': `Bearer ${llmApiKey}`,
|
|
1734
|
+
'Content-Type': 'application/json',
|
|
1735
|
+
};
|
|
1736
|
+
// OpenRouter requires additional headers
|
|
1737
|
+
if (activeLlm.provider === 'openrouter') {
|
|
1738
|
+
headers['HTTP-Referer'] = 'https://agentaudit.dev';
|
|
1739
|
+
headers['X-Title'] = 'AgentAudit CLI';
|
|
1740
|
+
}
|
|
1741
|
+
const res = await fetch(activeLlm.url, {
|
|
1742
|
+
method: 'POST',
|
|
1743
|
+
headers,
|
|
1744
|
+
body: JSON.stringify({
|
|
1745
|
+
model: activeLlm.model,
|
|
1746
|
+
max_tokens: 8192,
|
|
1747
|
+
messages: [
|
|
1748
|
+
{ role: 'system', content: systemPrompt },
|
|
1749
|
+
{ role: 'user', content: userMessage },
|
|
1750
|
+
],
|
|
1751
|
+
}),
|
|
1752
|
+
signal: AbortSignal.timeout(120_000),
|
|
1753
|
+
});
|
|
1754
|
+
data = await res.json();
|
|
1755
|
+
if (data.error) {
|
|
1756
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1757
|
+
const friendly = formatApiError(data.error, activeLlm.provider, res.status);
|
|
1758
|
+
if (friendly) {
|
|
1759
|
+
console.log(` ${c.red}${friendly.text}${c.reset}`);
|
|
1760
|
+
console.log(` ${c.dim}${friendly.hint}${c.reset}`);
|
|
1761
|
+
} else {
|
|
1762
|
+
console.log(` ${c.red}API error: ${data.error.message || JSON.stringify(data.error)}${c.reset}`);
|
|
1763
|
+
}
|
|
1764
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1765
|
+
return null;
|
|
1925
1766
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1767
|
+
_lastLlmText = data.choices?.[0]?.message?.content || '';
|
|
1768
|
+
report = extractJSON(_lastLlmText);
|
|
1769
|
+
if (report) {
|
|
1770
|
+
report.audit_model = data.model || activeLlm.model;
|
|
1771
|
+
report.audit_provider = activeLlm.provider;
|
|
1772
|
+
if (data.id) report.provider_msg_id = data.id;
|
|
1773
|
+
if (data.system_fingerprint) report.provider_fingerprint = data.system_fingerprint;
|
|
1774
|
+
if (data.usage) {
|
|
1775
|
+
report.input_tokens = data.usage.prompt_tokens;
|
|
1776
|
+
report.output_tokens = data.usage.completion_tokens;
|
|
1777
|
+
}
|
|
1929
1778
|
}
|
|
1930
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1931
|
-
return null;
|
|
1932
1779
|
}
|
|
1933
1780
|
|
|
1934
|
-
report = result.report;
|
|
1935
|
-
providerMeta = result.meta;
|
|
1936
|
-
_lastLlmText = result.rawText || '';
|
|
1937
1781
|
console.log(` ${c.green}done${c.reset} ${c.dim}(${elapsed(start)})${c.reset}`);
|
|
1782
|
+
} catch (err) {
|
|
1783
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1784
|
+
if (err.name === 'TimeoutError' || err.message?.includes('timeout')) {
|
|
1785
|
+
console.log(` ${c.red}Request timed out (120s)${c.reset}`);
|
|
1786
|
+
console.log(` ${c.dim}The provider took too long to respond. Try again or use a faster model${c.reset}`);
|
|
1787
|
+
} else if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.message?.includes('fetch failed')) {
|
|
1788
|
+
console.log(` ${c.red}Network error: could not reach ${activeProvider}${c.reset}`);
|
|
1789
|
+
console.log(` ${c.dim}Check your internet connection or provider status${c.reset}`);
|
|
1790
|
+
} else {
|
|
1791
|
+
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
1792
|
+
}
|
|
1793
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1794
|
+
return null;
|
|
1938
1795
|
}
|
|
1939
1796
|
|
|
1940
1797
|
// Cleanup repo
|
|
1941
1798
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1942
1799
|
|
|
1943
1800
|
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) {
|
|
1801
|
+
console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
|
|
1802
|
+
console.log(` ${c.dim}Hint: run with --debug to see the raw LLM response${c.reset}`);
|
|
1803
|
+
if (process.argv.includes('--debug')) {
|
|
1957
1804
|
console.log(` ${c.dim}--- Raw LLM response (first 2000 chars) ---${c.reset}`);
|
|
1958
|
-
console.log(_lastLlmText.slice(0, 2000));
|
|
1805
|
+
console.log((typeof _lastLlmText === 'string' ? _lastLlmText : '(empty)').slice(0, 2000));
|
|
1959
1806
|
console.log(` ${c.dim}--- end ---${c.reset}`);
|
|
1960
1807
|
}
|
|
1961
1808
|
return null;
|
|
@@ -1963,19 +1810,8 @@ async function auditRepo(url) {
|
|
|
1963
1810
|
|
|
1964
1811
|
// Display results
|
|
1965
1812
|
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}`);
|
|
1813
|
+
const riskScore = report.risk_score || 0;
|
|
1814
|
+
console.log(` ${riskBadge(riskScore)} Risk ${riskScore}/100 ${c.bold}${report.result || 'unknown'}${c.reset}`);
|
|
1979
1815
|
console.log();
|
|
1980
1816
|
|
|
1981
1817
|
if (report.findings && report.findings.length > 0) {
|
|
@@ -1993,20 +1829,12 @@ async function auditRepo(url) {
|
|
|
1993
1829
|
console.log();
|
|
1994
1830
|
}
|
|
1995
1831
|
|
|
1996
|
-
// Upload to registry
|
|
1997
|
-
const
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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
|
-
if (creds) {
|
|
1832
|
+
// Upload to registry (skip with --no-upload)
|
|
1833
|
+
const noUpload = process.argv.includes('--no-upload');
|
|
1834
|
+
let creds = loadCredentials();
|
|
1835
|
+
if (noUpload) {
|
|
1836
|
+
// Skip silently
|
|
1837
|
+
} else if (creds) {
|
|
2010
1838
|
process.stdout.write(` Uploading report to registry...`);
|
|
2011
1839
|
try {
|
|
2012
1840
|
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
@@ -2015,86 +1843,64 @@ async function auditRepo(url) {
|
|
|
2015
1843
|
'Authorization': `Bearer ${creds.api_key}`,
|
|
2016
1844
|
'Content-Type': 'application/json',
|
|
2017
1845
|
},
|
|
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
|
-
}),
|
|
1846
|
+
body: JSON.stringify(report),
|
|
2031
1847
|
signal: AbortSignal.timeout(15_000),
|
|
2032
1848
|
});
|
|
2033
1849
|
if (res.ok) {
|
|
2034
1850
|
const data = await res.json();
|
|
2035
|
-
const reportSlug = data?.skill_slug || data?.slug || slug;
|
|
2036
1851
|
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 {}
|
|
1852
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
|
|
2087
1853
|
} else {
|
|
2088
|
-
const errBody = await res.text().catch(() => '');
|
|
2089
1854
|
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
1855
|
}
|
|
2092
1856
|
} catch (err) {
|
|
2093
1857
|
console.log(` ${c.yellow}failed${c.reset}`);
|
|
2094
|
-
|
|
1858
|
+
}
|
|
1859
|
+
} else if (process.stdin.isTTY) {
|
|
1860
|
+
// No credentials — offer inline registration
|
|
1861
|
+
console.log();
|
|
1862
|
+
console.log(` ${c.bold}Share this audit with the community?${c.reset}`);
|
|
1863
|
+
console.log(` ${c.dim}Uploading helps others assess package security. Account is free.${c.reset}`);
|
|
1864
|
+
console.log();
|
|
1865
|
+
console.log(` ${c.bold}1)${c.reset} Create account + upload ${c.dim}(free)${c.reset}`);
|
|
1866
|
+
console.log(` ${c.bold}2)${c.reset} Skip`);
|
|
1867
|
+
console.log();
|
|
1868
|
+
const uploadChoice = await askQuestion(` Choice ${c.dim}(1/2)${c.reset}: `);
|
|
1869
|
+
if (uploadChoice === '1') {
|
|
1870
|
+
const name = await askQuestion(` Agent name ${c.dim}(e.g. my-scanner, claude-desktop)${c.reset}: `);
|
|
1871
|
+
if (!name || !/^[a-zA-Z0-9._-]{2,64}$/.test(name)) {
|
|
1872
|
+
console.log(` ${c.red}Invalid name. Use 2-64 chars: letters, numbers, dash, underscore, dot.${c.reset}`);
|
|
1873
|
+
} else {
|
|
1874
|
+
try {
|
|
1875
|
+
process.stdout.write(` Registering ${c.bold}${name}${c.reset}...`);
|
|
1876
|
+
const regData = await registerAgent(name);
|
|
1877
|
+
saveCredentials({ api_key: regData.api_key, agent_name: regData.agent_name });
|
|
1878
|
+
console.log(` ${c.green}done!${c.reset}`);
|
|
1879
|
+
creds = { api_key: regData.api_key, agent_name: regData.agent_name };
|
|
1880
|
+
process.stdout.write(` Uploading report...`);
|
|
1881
|
+
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
1882
|
+
method: 'POST',
|
|
1883
|
+
headers: {
|
|
1884
|
+
'Authorization': `Bearer ${creds.api_key}`,
|
|
1885
|
+
'Content-Type': 'application/json',
|
|
1886
|
+
},
|
|
1887
|
+
body: JSON.stringify(report),
|
|
1888
|
+
signal: AbortSignal.timeout(15_000),
|
|
1889
|
+
});
|
|
1890
|
+
if (res.ok) {
|
|
1891
|
+
console.log(` ${c.green}done${c.reset}`);
|
|
1892
|
+
console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
|
|
1893
|
+
} else {
|
|
1894
|
+
console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
|
|
1895
|
+
}
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
console.log(` ${c.red}failed${c.reset}`);
|
|
1898
|
+
console.log(` ${c.dim}${err.message}${c.reset}`);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
2095
1901
|
}
|
|
2096
1902
|
} else {
|
|
2097
|
-
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to
|
|
1903
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to create an account and upload reports${c.reset}`);
|
|
2098
1904
|
}
|
|
2099
1905
|
|
|
2100
1906
|
console.log();
|
|
@@ -2103,89 +1909,28 @@ async function auditRepo(url) {
|
|
|
2103
1909
|
|
|
2104
1910
|
// ── Check command ───────────────────────────────────────
|
|
2105
1911
|
|
|
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();
|
|
1912
|
+
async function checkPackage(name) {
|
|
2109
1913
|
if (!jsonMode) {
|
|
2110
1914
|
console.log(`${icons.info} Looking up ${c.bold}${name}${c.reset} in registry...`);
|
|
2111
1915
|
console.log();
|
|
2112
1916
|
}
|
|
2113
1917
|
|
|
2114
|
-
const data = await checkRegistry(
|
|
1918
|
+
const data = await checkRegistry(name);
|
|
2115
1919
|
if (!data) {
|
|
2116
1920
|
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}`);
|
|
1921
|
+
console.log(` ${c.yellow}Not found${c.reset} — package "${name}" hasn't been audited yet.`);
|
|
1922
|
+
console.log(` ${c.dim}Run: agentaudit audit <repo-url> for a deep LLM audit${c.reset}`);
|
|
2130
1923
|
}
|
|
2131
1924
|
return null;
|
|
2132
1925
|
}
|
|
2133
1926
|
|
|
2134
1927
|
if (!jsonMode) {
|
|
2135
1928
|
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}`);
|
|
1929
|
+
console.log(` ${c.bold}${name}${c.reset} ${riskBadge(riskScore)}`);
|
|
1930
|
+
console.log(` ${c.dim}Risk Score: ${riskScore}/100${c.reset}`);
|
|
1931
|
+
if (data.source_url) console.log(` ${c.dim}Source: ${data.source_url}${c.reset}`);
|
|
1932
|
+
console.log(` ${c.dim}Registry: ${REGISTRY_URL}/skills/${name}${c.reset}`);
|
|
2172
1933
|
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
1934
|
console.log();
|
|
2190
1935
|
}
|
|
2191
1936
|
return data;
|
|
@@ -2207,40 +1952,12 @@ async function main() {
|
|
|
2207
1952
|
quietMode = rawArgs.includes('--quiet') || rawArgs.includes('-q');
|
|
2208
1953
|
// --no-color already handled at top level for `c` object
|
|
2209
1954
|
|
|
2210
|
-
//
|
|
2211
|
-
const
|
|
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
|
|
2233
|
-
const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color']);
|
|
1955
|
+
// Strip global flags from args (including --model <value>)
|
|
1956
|
+
const globalFlags = new Set(['--json', '--quiet', '-q', '--no-color', '--no-upload']);
|
|
2234
1957
|
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
|
-
});
|
|
1958
|
+
// Remove --model <value> pair
|
|
1959
|
+
const modelIdx = args.indexOf('--model');
|
|
1960
|
+
if (modelIdx !== -1) args.splice(modelIdx, 2);
|
|
2244
1961
|
|
|
2245
1962
|
if (args[0] === '-v' || args[0] === '--version') {
|
|
2246
1963
|
console.log(`agentaudit ${getVersion()}`);
|
|
@@ -2248,46 +1965,47 @@ async function main() {
|
|
|
2248
1965
|
}
|
|
2249
1966
|
|
|
2250
1967
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
2251
|
-
|
|
2252
|
-
console.log(` ${c.bold}
|
|
2253
|
-
console.log(
|
|
1968
|
+
banner();
|
|
1969
|
+
console.log(` ${c.bold}Commands:${c.reset}`);
|
|
1970
|
+
console.log();
|
|
1971
|
+
console.log(` ${c.cyan}agentaudit${c.reset} Discover MCP servers (same as discover)`);
|
|
1972
|
+
console.log(` ${c.cyan}agentaudit discover${c.reset} Find MCP servers in your AI editors (Cursor, Claude, VS Code, Windsurf)`);
|
|
1973
|
+
console.log(` ${c.cyan}agentaudit discover --quick${c.reset} Discover + auto-scan all servers`);
|
|
1974
|
+
console.log(` ${c.cyan}agentaudit discover --deep${c.reset} Discover + select servers to deep-audit`);
|
|
1975
|
+
console.log(` ${c.cyan}agentaudit scan${c.reset} <url> [url...] Quick static scan (regex, local)`);
|
|
1976
|
+
console.log(` ${c.cyan}agentaudit scan${c.reset} <url> ${c.dim}--deep${c.reset} Deep audit (same as audit)`);
|
|
1977
|
+
console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
|
|
1978
|
+
console.log(` ${c.cyan}agentaudit lookup${c.reset} <name> Look up package in registry`);
|
|
1979
|
+
console.log(` ${c.cyan}agentaudit model${c.reset} Configure LLM provider + model interactively`);
|
|
1980
|
+
console.log(` ${c.cyan}agentaudit model${c.reset} <name> Set model directly (e.g. gpt-4o)`);
|
|
1981
|
+
console.log(` ${c.cyan}agentaudit setup${c.reset} Create account / enter API key (for report uploads)`);
|
|
2254
1982
|
console.log();
|
|
2255
|
-
console.log(` ${c.bold}
|
|
2256
|
-
console.log(` ${c.
|
|
2257
|
-
console.log(` ${c.
|
|
2258
|
-
console.log(` ${c.
|
|
1983
|
+
console.log(` ${c.bold}Global flags:${c.reset}`);
|
|
1984
|
+
console.log(` ${c.dim}--json Output JSON to stdout (machine-readable)${c.reset}`);
|
|
1985
|
+
console.log(` ${c.dim}--quiet Suppress banner and tree visualization${c.reset}`);
|
|
1986
|
+
console.log(` ${c.dim}--no-color Disable ANSI colors (also: NO_COLOR env)${c.reset}`);
|
|
1987
|
+
console.log(` ${c.dim}--model <name> Override LLM model for this run${c.reset}`);
|
|
1988
|
+
console.log(` ${c.dim}--no-upload Skip uploading report to registry${c.reset}`);
|
|
2259
1989
|
console.log();
|
|
2260
|
-
console.log(` ${c.bold}
|
|
2261
|
-
console.log(` ${c.
|
|
2262
|
-
console.log(` ${c.
|
|
1990
|
+
console.log(` ${c.bold}Quick Scan${c.reset} vs ${c.bold}Deep Audit${c.reset}:`);
|
|
1991
|
+
console.log(` ${c.dim}scan = fast regex-based static analysis (~2s)${c.reset}`);
|
|
1992
|
+
console.log(` ${c.dim}audit = deep LLM analysis with 3-pass methodology (~30s)${c.reset}`);
|
|
2263
1993
|
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`);
|
|
1994
|
+
console.log(` ${c.bold}Exit codes:${c.reset}`);
|
|
1995
|
+
console.log(` ${c.dim}0 = clean / success 1 = findings detected 2 = error${c.reset}`);
|
|
2269
1996
|
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}`);
|
|
1997
|
+
console.log(` ${c.bold}Examples:${c.reset}`);
|
|
1998
|
+
console.log(` agentaudit`);
|
|
1999
|
+
console.log(` agentaudit discover --quick`);
|
|
2000
|
+
console.log(` agentaudit scan https://github.com/owner/repo`);
|
|
2001
|
+
console.log(` agentaudit audit https://github.com/owner/repo`);
|
|
2002
|
+
console.log(` agentaudit lookup fastmcp --json`);
|
|
2278
2003
|
console.log();
|
|
2279
|
-
console.log(` ${c.bold}
|
|
2280
|
-
console.log(`
|
|
2281
|
-
console.log(` ${c.dim}$${c.reset} agentaudit audit https://github.com/owner/repo`);
|
|
2282
|
-
console.log(` ${c.dim}$${c.reset} agentaudit check fastmcp`);
|
|
2283
|
-
console.log(` ${c.dim}$${c.reset} agentaudit status`);
|
|
2004
|
+
console.log(` ${c.bold}For deep audits,${c.reset} set an LLM API key (e.g. ${c.cyan}export OPENROUTER_API_KEY=sk-or-...${c.reset})`);
|
|
2005
|
+
console.log(` ${c.dim}Run "agentaudit model" to configure provider + model interactively${c.reset}`);
|
|
2284
2006
|
console.log();
|
|
2285
|
-
console.log(` ${c.bold}
|
|
2286
|
-
console.log(`
|
|
2287
|
-
console.log(` ${c.dim}Set default: AGENTAUDIT_PROVIDER=openai AGENTAUDIT_MODEL=gpt-4o-mini${c.reset}`);
|
|
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}`);
|
|
2007
|
+
console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed):${c.reset}`);
|
|
2008
|
+
console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
|
|
2291
2009
|
console.log();
|
|
2292
2010
|
process.exitCode = 0; return;
|
|
2293
2011
|
}
|
|
@@ -2296,312 +2014,224 @@ async function main() {
|
|
|
2296
2014
|
const command = args.length === 0 ? 'discover' : args[0];
|
|
2297
2015
|
const targets = args.slice(1);
|
|
2298
2016
|
|
|
2299
|
-
|
|
2017
|
+
banner();
|
|
2300
2018
|
|
|
2301
2019
|
if (command === 'setup') {
|
|
2302
2020
|
await setupCommand();
|
|
2303
2021
|
return;
|
|
2304
2022
|
}
|
|
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
2023
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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`);
|
|
2024
|
+
if (command === 'model') {
|
|
2025
|
+
const newModel = targets.filter(t => !t.startsWith('--'))[0];
|
|
2026
|
+
const config = loadLlmConfig();
|
|
2027
|
+
const current = config?.llm_model;
|
|
2028
|
+
const currentProvider = config?.preferred_provider;
|
|
2029
|
+
|
|
2030
|
+
// Direct set: agentaudit model reset
|
|
2031
|
+
if (newModel === 'reset' || newModel === 'clear') {
|
|
2032
|
+
for (const f of [USER_CRED_FILE, SKILL_CRED_FILE]) {
|
|
2033
|
+
if (fs.existsSync(f)) {
|
|
2034
|
+
try {
|
|
2035
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
2036
|
+
delete data.llm_model;
|
|
2037
|
+
delete data.preferred_provider;
|
|
2038
|
+
fs.writeFileSync(f, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
2039
|
+
} catch {}
|
|
2363
2040
|
}
|
|
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
2041
|
}
|
|
2042
|
+
console.log(` ${icons.safe} Model + provider reset to defaults`);
|
|
2043
|
+
return;
|
|
2367
2044
|
}
|
|
2368
2045
|
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
console.log(`
|
|
2374
|
-
|
|
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}`);
|
|
2046
|
+
// Direct set: agentaudit model <name> (sets model only, keeps provider)
|
|
2047
|
+
if (newModel) {
|
|
2048
|
+
saveLlmConfig(newModel);
|
|
2049
|
+
console.log(` ${icons.safe} Model set to ${c.bold}${newModel}${c.reset}`);
|
|
2050
|
+
if (current) console.log(` ${c.dim}Was: ${current}${c.reset}`);
|
|
2051
|
+
return;
|
|
2381
2052
|
}
|
|
2382
2053
|
|
|
2383
|
-
//
|
|
2054
|
+
// ── No argument — interactive two-step menu ──
|
|
2055
|
+
|
|
2056
|
+
// Build deduplicated provider list
|
|
2057
|
+
const seen = new Set();
|
|
2058
|
+
const providerList = [];
|
|
2059
|
+
for (const p of LLM_PROVIDERS) {
|
|
2060
|
+
if (seen.has(p.provider)) continue;
|
|
2061
|
+
seen.add(p.provider);
|
|
2062
|
+
// Check if ANY key for this provider is set
|
|
2063
|
+
const keys = LLM_PROVIDERS.filter(x => x.provider === p.provider);
|
|
2064
|
+
const hasKey = keys.some(x => process.env[x.key]);
|
|
2065
|
+
const keyName = p.key;
|
|
2066
|
+
providerList.push({
|
|
2067
|
+
label: p.name,
|
|
2068
|
+
sublabel: hasKey
|
|
2069
|
+
? `${keyName} ${c.green}✔${c.reset}`
|
|
2070
|
+
: `${keyName} ${c.red}✗${c.reset} ${c.dim}set: export ${keyName}=...${c.reset}`,
|
|
2071
|
+
value: p.provider,
|
|
2072
|
+
hasKey,
|
|
2073
|
+
keyName,
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// Show status
|
|
2078
|
+
const resolved = resolveProvider();
|
|
2079
|
+
console.log(` ${c.bold}LLM Configuration${c.reset}`);
|
|
2384
2080
|
console.log();
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
console.log(`
|
|
2390
|
-
|
|
2391
|
-
|
|
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 {}
|
|
2081
|
+
if (currentProvider && current) {
|
|
2082
|
+
const provInfo = LLM_PROVIDERS.find(p => p.provider === currentProvider);
|
|
2083
|
+
console.log(` Active: ${c.bold}${c.cyan}${provInfo?.name || currentProvider} → ${current}${c.reset}`);
|
|
2084
|
+
} else if (current) {
|
|
2085
|
+
console.log(` Active: ${c.bold}${c.cyan}${resolved?.name || 'auto'} → ${current}${c.reset}`);
|
|
2086
|
+
} else if (resolved) {
|
|
2087
|
+
console.log(` Active: ${c.dim}${resolved.name} → ${resolved.model} (defaults)${c.reset}`);
|
|
2412
2088
|
} else {
|
|
2413
|
-
console.log(`
|
|
2089
|
+
console.log(` Active: ${c.yellow}no provider configured${c.reset}`);
|
|
2414
2090
|
}
|
|
2415
2091
|
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
|
-
|
|
2092
|
+
|
|
2093
|
+
// Step A: Provider selection
|
|
2094
|
+
const providerChoices = [
|
|
2095
|
+
...providerList,
|
|
2096
|
+
{ label: '(keep current)', sublabel: '', value: '__keep__', hasKey: true },
|
|
2097
|
+
];
|
|
2098
|
+
|
|
2099
|
+
const selectedProvider = await singleSelect(providerChoices, {
|
|
2100
|
+
title: 'Choose provider',
|
|
2101
|
+
hint: '↑↓ move Enter select Esc cancel',
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
if (selectedProvider === null) {
|
|
2105
|
+
console.log(` ${c.dim}Cancelled${c.reset}`);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (selectedProvider === '__keep__') {
|
|
2110
|
+
console.log(` ${c.dim}Keeping current config${c.reset}`);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Check if provider has key set
|
|
2115
|
+
const chosenEntry = providerList.find(p => p.value === selectedProvider);
|
|
2116
|
+
if (chosenEntry && !chosenEntry.hasKey) {
|
|
2117
|
+
console.log();
|
|
2118
|
+
console.log(` ${c.yellow}${chosenEntry.keyName} is not set.${c.reset}`);
|
|
2119
|
+
console.log(` ${c.dim}Run: export ${chosenEntry.keyName}=your-key-here${c.reset}`);
|
|
2120
|
+
console.log(` ${c.dim}Then run "agentaudit model" again.${c.reset}`);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Step B: Model selection for chosen provider
|
|
2449
2125
|
console.log();
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
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}`);
|
|
2126
|
+
const providerModels = PROVIDER_MODELS[selectedProvider] || [];
|
|
2127
|
+
const modelChoices = [
|
|
2128
|
+
...providerModels,
|
|
2129
|
+
{ label: '(enter custom model name)', sublabel: '', value: '__custom__' },
|
|
2130
|
+
{ label: '(reset to provider default)', sublabel: '', value: '__reset__' },
|
|
2131
|
+
];
|
|
2132
|
+
|
|
2133
|
+
const providerName = LLM_PROVIDERS.find(p => p.provider === selectedProvider)?.name || selectedProvider;
|
|
2134
|
+
const selectedModel = await singleSelect(modelChoices, {
|
|
2135
|
+
title: `Choose model for ${providerName}`,
|
|
2136
|
+
hint: '↑↓ move Enter select Esc cancel',
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
if (selectedModel === null) {
|
|
2140
|
+
console.log(` ${c.dim}Cancelled${c.reset}`);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
if (selectedModel === '__reset__') {
|
|
2145
|
+
// Save provider, clear model (use provider default)
|
|
2146
|
+
saveLlmConfig(undefined, selectedProvider);
|
|
2147
|
+
// Also clear llm_model from both files
|
|
2148
|
+
for (const f of [USER_CRED_FILE, SKILL_CRED_FILE]) {
|
|
2149
|
+
if (fs.existsSync(f)) {
|
|
2150
|
+
try {
|
|
2151
|
+
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
2152
|
+
delete data.llm_model;
|
|
2153
|
+
data.preferred_provider = selectedProvider;
|
|
2154
|
+
fs.writeFileSync(f, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
2155
|
+
} catch {}
|
|
2496
2156
|
}
|
|
2497
|
-
} catch (e) {
|
|
2498
|
-
process.stdout.write(`\r ${c.red}Failed to fetch: ${e.message}${c.reset} \n`);
|
|
2499
2157
|
}
|
|
2500
|
-
|
|
2501
|
-
console.log(
|
|
2502
|
-
console.log(`
|
|
2503
|
-
|
|
2158
|
+
const defaultModel = LLM_PROVIDERS.find(p => p.provider === selectedProvider)?.model;
|
|
2159
|
+
console.log();
|
|
2160
|
+
console.log(` ${icons.safe} Provider: ${c.bold}${providerName}${c.reset}, model: ${c.dim}${defaultModel} (default)${c.reset}`);
|
|
2161
|
+
return;
|
|
2504
2162
|
}
|
|
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}`);
|
|
2163
|
+
|
|
2164
|
+
if (selectedModel === '__custom__') {
|
|
2165
|
+
console.log();
|
|
2166
|
+
const custom = await askQuestion(` Model name: `);
|
|
2167
|
+
if (!custom) {
|
|
2168
|
+
console.log(` ${c.dim}Cancelled${c.reset}`);
|
|
2169
|
+
return;
|
|
2520
2170
|
}
|
|
2521
|
-
|
|
2522
|
-
console.log(`
|
|
2171
|
+
saveLlmConfig(custom, selectedProvider);
|
|
2172
|
+
console.log(` ${icons.safe} Provider: ${c.bold}${providerName}${c.reset}, model: ${c.bold}${custom}${c.reset}`);
|
|
2173
|
+
if (current) console.log(` ${c.dim}Was: ${currentProvider || 'auto'} → ${current}${c.reset}`);
|
|
2174
|
+
return;
|
|
2523
2175
|
}
|
|
2176
|
+
|
|
2177
|
+
// Normal model selection
|
|
2178
|
+
saveLlmConfig(selectedModel, selectedProvider);
|
|
2524
2179
|
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}`);
|
|
2180
|
+
console.log(` ${icons.safe} Provider: ${c.bold}${providerName}${c.reset}, model: ${c.bold}${selectedModel}${c.reset}`);
|
|
2181
|
+
if (current) console.log(` ${c.dim}Was: ${currentProvider || 'auto'} → ${current}${c.reset}`);
|
|
2182
|
+
console.log();
|
|
2183
|
+
console.log(` ${c.dim}Tip: agentaudit model <name> to set model directly, or agentaudit model reset${c.reset}`);
|
|
2530
2184
|
return;
|
|
2531
2185
|
}
|
|
2532
2186
|
|
|
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
|
-
}
|
|
2187
|
+
if (command === 'discover') {
|
|
2188
|
+
const scanFlag = targets.includes('--quick') || targets.includes('--scan') || targets.includes('-s');
|
|
2189
|
+
const auditFlag = targets.includes('--deep') || targets.includes('--audit') || targets.includes('-a');
|
|
2190
|
+
await discoverCommand({ scan: scanFlag, audit: auditFlag });
|
|
2572
2191
|
return;
|
|
2573
2192
|
}
|
|
2574
2193
|
|
|
2575
2194
|
if (command === 'lookup' || command === 'check') {
|
|
2576
2195
|
const names = targets.filter(t => !t.startsWith('--'));
|
|
2577
2196
|
if (names.length === 0) {
|
|
2578
|
-
console.log(` ${c.red}
|
|
2579
|
-
console.log(` ${c.dim}Usage: agentaudit check <name|url>${c.reset}`);
|
|
2197
|
+
console.log(` ${c.red}Error: package name required${c.reset}`);
|
|
2580
2198
|
process.exitCode = 2;
|
|
2581
2199
|
return;
|
|
2582
2200
|
}
|
|
2583
2201
|
const results = [];
|
|
2584
|
-
const allowAutoAudit = command === 'check'; // only 'check' auto-audits, 'lookup' never does
|
|
2585
2202
|
for (const t of names) {
|
|
2586
|
-
const data = await checkPackage(t
|
|
2203
|
+
const data = await checkPackage(t);
|
|
2587
2204
|
results.push(data);
|
|
2588
2205
|
}
|
|
2589
2206
|
if (jsonMode) {
|
|
2590
2207
|
console.log(JSON.stringify(results.length === 1 ? (results[0] || { error: 'not_found' }) : results, null, 2));
|
|
2591
2208
|
}
|
|
2592
2209
|
process.exitCode = 0; return;
|
|
2210
|
+
return;
|
|
2593
2211
|
}
|
|
2594
2212
|
|
|
2595
2213
|
if (command === 'scan') {
|
|
2214
|
+
const deepFlag = targets.includes('--deep');
|
|
2596
2215
|
const urls = targets.filter(t => !t.startsWith('--'));
|
|
2597
2216
|
if (urls.length === 0) {
|
|
2598
|
-
console.log(` ${c.red}
|
|
2599
|
-
console.log(`
|
|
2600
|
-
console.log(`
|
|
2217
|
+
console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
|
|
2218
|
+
console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit discover${c.dim} to find & check locally installed MCP servers${c.reset}`);
|
|
2219
|
+
console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit audit <url>${c.dim} for a deep LLM-powered audit${c.reset}`);
|
|
2601
2220
|
process.exitCode = 2;
|
|
2602
2221
|
return;
|
|
2603
2222
|
}
|
|
2604
2223
|
|
|
2224
|
+
// --deep redirects to audit flow
|
|
2225
|
+
if (deepFlag) {
|
|
2226
|
+
let hasFindings = false;
|
|
2227
|
+
for (const url of urls) {
|
|
2228
|
+
const report = await auditRepo(url);
|
|
2229
|
+
if (report?.findings?.length > 0) hasFindings = true;
|
|
2230
|
+
}
|
|
2231
|
+
process.exitCode = hasFindings ? 1 : 0;
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2605
2235
|
const results = [];
|
|
2606
2236
|
let hadErrors = false;
|
|
2607
2237
|
for (const url of urls) {
|
|
@@ -2630,40 +2260,30 @@ async function main() {
|
|
|
2630
2260
|
}
|
|
2631
2261
|
|
|
2632
2262
|
if (hadErrors && results.length === 0) { process.exitCode = 2; return; }
|
|
2633
|
-
|
|
2263
|
+
const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2264
|
+
process.exitCode = totalFindings > 0 ? 1 : 0;
|
|
2634
2265
|
return;
|
|
2635
2266
|
}
|
|
2636
2267
|
|
|
2637
2268
|
if (command === 'audit') {
|
|
2638
2269
|
const urls = targets.filter(t => !t.startsWith('--'));
|
|
2639
2270
|
if (urls.length === 0) {
|
|
2640
|
-
console.log(` ${c.red}
|
|
2641
|
-
console.log(` ${c.dim}Usage: agentaudit audit <url>${c.reset}`);
|
|
2271
|
+
console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
|
|
2642
2272
|
process.exitCode = 2;
|
|
2643
2273
|
return;
|
|
2644
2274
|
}
|
|
2645
2275
|
|
|
2646
|
-
let
|
|
2276
|
+
let hasFindings = false;
|
|
2647
2277
|
for (const url of urls) {
|
|
2648
2278
|
const report = await auditRepo(url);
|
|
2649
|
-
if (
|
|
2279
|
+
if (report?.findings?.length > 0) hasFindings = true;
|
|
2650
2280
|
}
|
|
2651
|
-
process.exitCode =
|
|
2281
|
+
process.exitCode = hasFindings ? 1 : 0;
|
|
2652
2282
|
return;
|
|
2653
2283
|
}
|
|
2654
2284
|
|
|
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}`);
|
|
2285
|
+
console.log(` ${c.red}Unknown command: ${command}${c.reset}`);
|
|
2286
|
+
console.log(` ${c.dim}Run agentaudit --help for usage${c.reset}`);
|
|
2667
2287
|
process.exitCode = 2;
|
|
2668
2288
|
}
|
|
2669
2289
|
|