agentaudit 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs ADDED
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentAudit MCP Server
4
+ *
5
+ * Provides security audit capabilities via Model Context Protocol.
6
+ *
7
+ * Tools:
8
+ * - audit_package: Clone a repo, read source files, return with audit prompt
9
+ * - submit_report: Upload a completed audit report to agentaudit.dev
10
+ * - check_package: Look up a package in the AgentAudit registry
11
+ *
12
+ * Usage:
13
+ * node mcp-server/index.mjs
14
+ *
15
+ * Configure in Claude/Cursor/etc:
16
+ * {
17
+ * "mcpServers": {
18
+ * "agentaudit": {
19
+ * "command": "node",
20
+ * "args": ["path/to/agentaudit/mcp-server/index.mjs"]
21
+ * }
22
+ * }
23
+ * }
24
+ */
25
+
26
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
27
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
28
+ import {
29
+ CallToolRequestSchema,
30
+ ListToolsRequestSchema,
31
+ } from '@modelcontextprotocol/sdk/types.js';
32
+ import fs from 'fs';
33
+ import path from 'path';
34
+ import { execSync } from 'child_process';
35
+ import { fileURLToPath } from 'url';
36
+
37
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
+ const SKILL_DIR = path.resolve(__dirname);
39
+ const REGISTRY_URL = 'https://agentaudit.dev';
40
+ const MAX_FILE_SIZE = 50_000; // 50KB per file
41
+ const MAX_TOTAL_SIZE = 300_000; // 300KB total code
42
+ const SKIP_DIRS = new Set([
43
+ 'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
44
+ '.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
45
+ 'test', 'tests', '__tests__', 'spec', 'specs', 'docs', 'doc',
46
+ 'examples', 'example', 'fixtures', '.github', '.vscode', '.idea',
47
+ 'e2e', 'benchmark', 'benchmarks', '.tox', '.eggs', 'htmlcov',
48
+ ]);
49
+ const SKIP_EXTENSIONS = new Set([
50
+ '.lock', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff',
51
+ '.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.tar', '.gz',
52
+ '.map', '.min.js', '.min.css', '.d.ts', '.pyc', '.pyo', '.so',
53
+ '.dylib', '.dll', '.exe', '.bin', '.dat', '.db', '.sqlite',
54
+ ]);
55
+ const PRIORITY_FILES = [
56
+ 'index.js', 'index.ts', 'index.mjs', 'main.js', 'main.ts', 'main.py',
57
+ 'app.js', 'app.ts', 'app.py', 'server.js', 'server.ts', 'server.py',
58
+ 'cli.js', 'cli.ts', 'cli.py', '__init__.py', '__main__.py',
59
+ 'package.json', 'pyproject.toml', 'setup.py', 'setup.cfg',
60
+ 'Cargo.toml', 'go.mod', 'SKILL.md', 'skill.md',
61
+ 'Makefile', 'Dockerfile', 'docker-compose.yml',
62
+ ];
63
+
64
+ // ── Helpers ──────────────────────────────────────────────
65
+
66
+ function loadApiKey() {
67
+ if (process.env.AGENTAUDIT_API_KEY) return process.env.AGENTAUDIT_API_KEY;
68
+ const home = process.env.HOME || process.env.USERPROFILE || '';
69
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
70
+ const paths = [
71
+ path.join(SKILL_DIR, 'config', 'credentials.json'),
72
+ path.join(xdg, 'agentaudit', 'credentials.json'),
73
+ ];
74
+ for (const p of paths) {
75
+ if (fs.existsSync(p)) {
76
+ try {
77
+ const key = JSON.parse(fs.readFileSync(p, 'utf8')).api_key;
78
+ if (key) return key;
79
+ } catch {}
80
+ }
81
+ }
82
+ return '';
83
+ }
84
+
85
+ function loadAuditPrompt() {
86
+ const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
87
+ if (fs.existsSync(promptPath)) {
88
+ return fs.readFileSync(promptPath, 'utf8');
89
+ }
90
+ return 'ERROR: audit-prompt.md not found at ' + promptPath;
91
+ }
92
+
93
+ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
94
+ if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
95
+
96
+ let entries;
97
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
98
+ catch { return collected; }
99
+
100
+ // Sort: priority files first
101
+ entries.sort((a, b) => {
102
+ const aP = PRIORITY_FILES.includes(a.name) ? 0 : 1;
103
+ const bP = PRIORITY_FILES.includes(b.name) ? 0 : 1;
104
+ return aP - bP || a.name.localeCompare(b.name);
105
+ });
106
+
107
+ for (const entry of entries) {
108
+ if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
109
+
110
+ const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
111
+ const fullPath = path.join(dir, entry.name);
112
+
113
+ if (entry.isDirectory()) {
114
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
115
+ collectFiles(fullPath, relPath, collected, totalSize);
116
+ } else {
117
+ const ext = path.extname(entry.name).toLowerCase();
118
+ if (SKIP_EXTENSIONS.has(ext)) continue;
119
+
120
+ try {
121
+ const stat = fs.statSync(fullPath);
122
+ if (stat.size > MAX_FILE_SIZE) {
123
+ collected.push({ path: relPath, content: `[FILE TOO LARGE: ${stat.size} bytes — skipped]` });
124
+ continue;
125
+ }
126
+ if (stat.size === 0) continue;
127
+
128
+ const content = fs.readFileSync(fullPath, 'utf8');
129
+ totalSize.bytes += content.length;
130
+ collected.push({ path: relPath, content });
131
+ } catch {
132
+ // Binary or unreadable — skip
133
+ }
134
+ }
135
+ }
136
+ return collected;
137
+ }
138
+
139
+ function cloneRepo(sourceUrl) {
140
+ const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
141
+ try {
142
+ execSync(`git clone --depth 1 "${sourceUrl}" "${tmpDir}/repo" 2>/dev/null`, {
143
+ timeout: 30_000,
144
+ stdio: 'pipe',
145
+ });
146
+ return path.join(tmpDir, 'repo');
147
+ } catch (err) {
148
+ throw new Error(`Failed to clone ${sourceUrl}: ${err.message}`);
149
+ }
150
+ }
151
+
152
+ function cleanupRepo(repoPath) {
153
+ try {
154
+ execSync(`rm -rf "${path.dirname(repoPath)}"`, { stdio: 'pipe' });
155
+ } catch {}
156
+ }
157
+
158
+ function slugFromUrl(url) {
159
+ // https://github.com/owner/repo → owner-repo or just repo
160
+ const match = url.match(/github\.com\/([^/]+)\/([^/.\s]+)/);
161
+ if (match) return match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
162
+ return url.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60);
163
+ }
164
+
165
+ // ── MCP Server ───────────────────────────────────────────
166
+
167
+ const server = new Server(
168
+ { name: 'agentaudit', version: '1.0.0' },
169
+ { capabilities: { tools: {} } }
170
+ );
171
+
172
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
173
+ tools: [
174
+ {
175
+ name: 'audit_package',
176
+ description: 'Clone a repository and prepare it for security audit. Returns the source code and audit instructions. You (the agent) then analyze the code following the audit prompt and call submit_report with the results.',
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
180
+ source_url: {
181
+ type: 'string',
182
+ description: 'Git repository URL to audit (e.g., https://github.com/owner/repo)',
183
+ },
184
+ },
185
+ required: ['source_url'],
186
+ },
187
+ },
188
+ {
189
+ name: 'submit_report',
190
+ description: 'Submit a completed security audit report to the AgentAudit registry (agentaudit.dev). Call this after you have analyzed the code from audit_package.',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ report: {
195
+ type: 'object',
196
+ description: 'The audit report JSON object. Must include: skill_slug, source_url, risk_score (0-100), result (safe|caution|unsafe), findings (array), findings_count, max_severity.',
197
+ },
198
+ },
199
+ required: ['report'],
200
+ },
201
+ },
202
+ {
203
+ name: 'check_package',
204
+ description: 'Look up a package in the AgentAudit security registry. Returns the latest audit results if available.',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ package_name: {
209
+ type: 'string',
210
+ description: 'Package name or slug to look up (e.g., "fastmcp", "mongodb-mcp-server")',
211
+ },
212
+ },
213
+ required: ['package_name'],
214
+ },
215
+ },
216
+ ],
217
+ }));
218
+
219
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
220
+ const { name, arguments: args } = request.params;
221
+
222
+ switch (name) {
223
+ case 'audit_package': {
224
+ const { source_url } = args;
225
+ if (!source_url || !source_url.startsWith('http')) {
226
+ return { content: [{ type: 'text', text: 'Error: source_url must be a valid HTTP(S) URL' }] };
227
+ }
228
+
229
+ let repoPath;
230
+ try {
231
+ repoPath = cloneRepo(source_url);
232
+ const files = collectFiles(repoPath);
233
+ const slug = slugFromUrl(source_url);
234
+ const auditPrompt = loadAuditPrompt();
235
+
236
+ // Build the response
237
+ let codeBlock = '';
238
+ for (const file of files) {
239
+ codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
240
+ }
241
+
242
+ const response = [
243
+ `# Security Audit Request`,
244
+ ``,
245
+ `**Package:** ${slug}`,
246
+ `**Source:** ${source_url}`,
247
+ `**Files collected:** ${files.length}`,
248
+ ``,
249
+ `## Instructions`,
250
+ ``,
251
+ `Analyze the source code below following the audit methodology. After your analysis, call the \`submit_report\` tool with your findings as a JSON object.`,
252
+ ``,
253
+ `The report JSON must include:`,
254
+ '```json',
255
+ `{`,
256
+ ` "skill_slug": "${slug}",`,
257
+ ` "source_url": "${source_url}",`,
258
+ ` "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
259
+ ` "risk_score": <0-100>,`,
260
+ ` "result": "<safe|caution|unsafe>",`,
261
+ ` "max_severity": "<none|low|medium|high|critical>",`,
262
+ ` "findings_count": <number>,`,
263
+ ` "findings": [`,
264
+ ` {`,
265
+ ` "id": "FINDING_ID",`,
266
+ ` "title": "Short title",`,
267
+ ` "severity": "<low|medium|high|critical>",`,
268
+ ` "category": "<category>",`,
269
+ ` "description": "Detailed description",`,
270
+ ` "file": "path/to/file.js",`,
271
+ ` "line": <line_number>,`,
272
+ ` "remediation": "How to fix",`,
273
+ ` "confidence": "<low|medium|high>",`,
274
+ ` "is_by_design": <true|false>`,
275
+ ` }`,
276
+ ` ]`,
277
+ `}`,
278
+ '```',
279
+ ``,
280
+ `## Audit Methodology`,
281
+ ``,
282
+ auditPrompt,
283
+ ``,
284
+ `## Source Code`,
285
+ ``,
286
+ codeBlock,
287
+ ].join('\n');
288
+
289
+ return { content: [{ type: 'text', text: response }] };
290
+ } catch (err) {
291
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
292
+ } finally {
293
+ if (repoPath) cleanupRepo(repoPath);
294
+ }
295
+ }
296
+
297
+ case 'submit_report': {
298
+ const { report } = args;
299
+ if (!report || typeof report !== 'object') {
300
+ return { content: [{ type: 'text', text: 'Error: report must be a JSON object' }] };
301
+ }
302
+
303
+ const apiKey = loadApiKey();
304
+ if (!apiKey) {
305
+ return { content: [{ type: 'text', text: 'Error: No API key configured. Set AGENTAUDIT_API_KEY or register first.' }] };
306
+ }
307
+
308
+ // Validate required fields
309
+ const required = ['skill_slug', 'source_url', 'risk_score', 'result'];
310
+ for (const field of required) {
311
+ if (report[field] == null) {
312
+ return { content: [{ type: 'text', text: `Error: Missing required field "${field}" in report` }] };
313
+ }
314
+ }
315
+
316
+ // Auto-fix findings
317
+ if (!Array.isArray(report.findings)) report.findings = [];
318
+ report.findings_count = report.findings.length;
319
+ if (!report.max_severity) {
320
+ const severities = ['critical', 'high', 'medium', 'low', 'none'];
321
+ report.max_severity = report.findings.reduce((max, f) => {
322
+ const fi = severities.indexOf(f.severity);
323
+ const mi = severities.indexOf(max);
324
+ return fi < mi ? f.severity : max;
325
+ }, 'none');
326
+ }
327
+
328
+ try {
329
+ const res = await fetch(`${REGISTRY_URL}/api/reports`, {
330
+ method: 'POST',
331
+ headers: {
332
+ 'Authorization': `Bearer ${apiKey}`,
333
+ 'Content-Type': 'application/json',
334
+ },
335
+ body: JSON.stringify(report),
336
+ signal: AbortSignal.timeout(60_000),
337
+ });
338
+
339
+ const body = await res.text();
340
+ let data;
341
+ try { data = JSON.parse(body); } catch { data = { raw: body }; }
342
+
343
+ if (res.ok) {
344
+ return { content: [{ type: 'text', text: `Report submitted successfully!\nReport ID: ${data.report_id || 'unknown'}\nURL: ${REGISTRY_URL}/skills/${report.skill_slug}\n\n${JSON.stringify(data, null, 2)}` }] };
345
+ } else {
346
+ return { content: [{ type: 'text', text: `Upload failed (HTTP ${res.status}): ${JSON.stringify(data, null, 2)}` }] };
347
+ }
348
+ } catch (err) {
349
+ return { content: [{ type: 'text', text: `Upload error: ${err.message}` }] };
350
+ }
351
+ }
352
+
353
+ case 'check_package': {
354
+ const { package_name } = args;
355
+ if (!package_name) {
356
+ return { content: [{ type: 'text', text: 'Error: package_name is required' }] };
357
+ }
358
+
359
+ try {
360
+ const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(package_name)}`, {
361
+ signal: AbortSignal.timeout(10_000),
362
+ });
363
+
364
+ if (res.status === 404) {
365
+ return { content: [{ type: 'text', text: `Package "${package_name}" not found in registry. It may not have been audited yet. Use audit_package to audit it.` }] };
366
+ }
367
+
368
+ const data = await res.json();
369
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
370
+ } catch (err) {
371
+ return { content: [{ type: 'text', text: `Registry lookup failed: ${err.message}` }] };
372
+ }
373
+ }
374
+
375
+ default:
376
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
377
+ }
378
+ });
379
+
380
+ // ── Start ────────────────────────────────────────────────
381
+
382
+ const transport = new StdioServerTransport();
383
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "agentaudit",
3
+ "version": "3.0.0",
4
+ "description": "Security scanner for AI packages — MCP server + CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentaudit": "./cli.mjs"
8
+ },
9
+ "main": "index.mjs",
10
+ "files": [
11
+ "index.mjs",
12
+ "cli.mjs",
13
+ "prompts/audit-prompt.md",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "start": "node index.mjs",
19
+ "scan": "node cli.mjs scan"
20
+ },
21
+ "keywords": [
22
+ "security",
23
+ "audit",
24
+ "mcp",
25
+ "mcp-server",
26
+ "ai-agent",
27
+ "scanner",
28
+ "vulnerability",
29
+ "prompt-injection",
30
+ "agent-security"
31
+ ],
32
+ "author": "starbuck100",
33
+ "license": "AGPL-3.0",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/starbuck100/agentaudit-mcp.git"
37
+ },
38
+ "homepage": "https://agentaudit.dev",
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.0.0"
44
+ }
45
+ }