agentaudit 3.4.0 → 3.6.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.
Files changed (3) hide show
  1. package/cli.mjs +85 -9
  2. package/index.mjs +41 -4
  3. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import fs from 'fs';
17
+ import os from 'os';
17
18
  import path from 'path';
18
19
  import { execSync } from 'child_process';
19
20
  import { createInterface } from 'readline';
@@ -573,15 +574,18 @@ async function scanRepo(url) {
573
574
  process.stdout.write(`${icons.scan} Scanning ${c.bold}${slug}${c.reset} ${c.dim}...${c.reset}`);
574
575
 
575
576
  // Clone
576
- const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
577
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
577
578
  const repoPath = path.join(tmpDir, 'repo');
578
579
  try {
579
- execSync(`git clone --depth 1 "${url}" "${repoPath}" 2>/dev/null`, {
580
+ execSync(`git clone --depth 1 "${url}" "${repoPath}"`, {
580
581
  timeout: 30_000,
581
582
  stdio: 'pipe',
582
583
  });
583
584
  } catch (err) {
584
585
  process.stdout.write(` ${c.red}✖ clone failed${c.reset}\n`);
586
+ const msg = err.stderr?.toString().trim() || err.message?.split('\n')[0] || '';
587
+ if (msg) console.log(` ${c.dim}${msg}${c.reset}`);
588
+ console.log(` ${c.dim}Make sure git is installed and the URL is accessible.${c.reset}`);
585
589
  return null;
586
590
  }
587
591
 
@@ -598,7 +602,7 @@ async function scanRepo(url) {
598
602
  const registryData = await checkRegistry(slug);
599
603
 
600
604
  // Cleanup
601
- try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
605
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
602
606
 
603
607
  const duration = elapsed(start);
604
608
 
@@ -702,6 +706,19 @@ function extractServersFromConfig(config) {
702
706
  const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
703
707
  if (pyMatch) info.pyPackage = pyMatch[1];
704
708
 
709
+ // URL-based MCP server (remote HTTP)
710
+ if (info.url && !info.npmPackage && !info.pyPackage) {
711
+ try {
712
+ const parsed = new URL(info.url);
713
+ // Extract service name from hostname: mcp.supabase.com → supabase
714
+ const hostParts = parsed.hostname.split('.');
715
+ if (hostParts.length >= 2) {
716
+ const serviceName = hostParts.length === 3 ? hostParts[1] : hostParts[0];
717
+ info.remoteService = serviceName;
718
+ }
719
+ } catch {}
720
+ }
721
+
705
722
  result.push(info);
706
723
  }
707
724
  return result;
@@ -753,6 +770,39 @@ async function resolveSourceUrl(server) {
753
770
  return `https://pypi.org/project/${server.pyPackage}/`;
754
771
  }
755
772
 
773
+ // URL-based remote MCP server — try GitHub search by service name
774
+ if (server.remoteService) {
775
+ // Try npm registry with common MCP naming patterns
776
+ for (const tryName of [
777
+ `@${server.remoteService}/mcp-server-${server.remoteService}`,
778
+ `${server.remoteService}-mcp`,
779
+ `mcp-server-${server.remoteService}`,
780
+ server.remoteService,
781
+ ]) {
782
+ try {
783
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
784
+ signal: AbortSignal.timeout(3000),
785
+ });
786
+ if (res.ok) {
787
+ const data = await res.json();
788
+ let repoUrl = data.repository?.url;
789
+ if (repoUrl) {
790
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
791
+ if (repoUrl.startsWith('http')) return repoUrl;
792
+ }
793
+ }
794
+ } catch {}
795
+ }
796
+ }
797
+
798
+ // Last resort: if server has a url, show it as context
799
+ if (server.url) {
800
+ try {
801
+ const parsed = new URL(server.url);
802
+ return `https://github.com/search?q=${encodeURIComponent(parsed.hostname + ' MCP')}&type=repositories`;
803
+ } catch {}
804
+ }
805
+
756
806
  return null;
757
807
  }
758
808
 
@@ -821,6 +871,7 @@ async function discoverCommand() {
821
871
  let sourceLabel = '';
822
872
  if (server.npmPackage) sourceLabel = `${c.dim}npm:${server.npmPackage}${c.reset}`;
823
873
  else if (server.pyPackage) sourceLabel = `${c.dim}pip:${server.pyPackage}${c.reset}`;
874
+ else if (server.url) sourceLabel = `${c.dim}${server.url.length > 60 ? server.url.slice(0, 57) + '...' : server.url}${c.reset}`;
824
875
  else if (server.command) sourceLabel = `${c.dim}${[server.command, ...server.args.slice(0, 2)].join(' ')}${c.reset}`;
825
876
 
826
877
  if (regData) {
@@ -890,15 +941,18 @@ async function auditRepo(url) {
890
941
 
891
942
  // Step 1: Clone
892
943
  process.stdout.write(` ${c.dim}[1/4]${c.reset} Cloning repository...`);
893
- const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
944
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
894
945
  const repoPath = path.join(tmpDir, 'repo');
895
946
  try {
896
- execSync(`git clone --depth 1 "${url}" "${repoPath}" 2>/dev/null`, {
947
+ execSync(`git clone --depth 1 "${url}" "${repoPath}"`, {
897
948
  timeout: 30_000, stdio: 'pipe',
898
949
  });
899
950
  console.log(` ${c.green}done${c.reset}`);
900
- } catch {
951
+ } catch (err) {
901
952
  console.log(` ${c.red}failed${c.reset}`);
953
+ const msg = err.stderr?.toString().trim() || err.message?.split('\n')[0] || '';
954
+ if (msg) console.log(` ${c.dim}${msg}${c.reset}`);
955
+ console.log(` ${c.dim}Make sure git is installed and the URL is accessible.${c.reset}`);
902
956
  return null;
903
957
  }
904
958
 
@@ -938,6 +992,10 @@ async function auditRepo(url) {
938
992
  console.log(` ${c.dim}$env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
939
993
  console.log(` ${c.dim}$env:OPENAI_API_KEY = "sk-..."${c.reset}`);
940
994
  console.log();
995
+ console.log(` ${c.dim}# Windows (CMD):${c.reset}`);
996
+ console.log(` ${c.dim}set ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
997
+ console.log(` ${c.dim}set OPENAI_API_KEY=sk-...${c.reset}`);
998
+ console.log();
941
999
  console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
942
1000
  console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
943
1001
  console.log(` ${c.dim}Creates a markdown file you can paste into any LLM (Claude, ChatGPT, etc.)${c.reset}`);
@@ -976,7 +1034,7 @@ async function auditRepo(url) {
976
1034
  }
977
1035
 
978
1036
  // Cleanup
979
- try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
1037
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
980
1038
  return null;
981
1039
  }
982
1040
 
@@ -1050,12 +1108,12 @@ async function auditRepo(url) {
1050
1108
  } catch (err) {
1051
1109
  console.log(` ${c.red}failed${c.reset}`);
1052
1110
  console.log(` ${c.red}${err.message}${c.reset}`);
1053
- try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
1111
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1054
1112
  return null;
1055
1113
  }
1056
1114
 
1057
1115
  // Cleanup repo
1058
- try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
1116
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
1059
1117
 
1060
1118
  if (!report) {
1061
1119
  console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
@@ -1142,6 +1200,11 @@ async function checkPackage(name) {
1142
1200
  async function main() {
1143
1201
  const args = process.argv.slice(2);
1144
1202
 
1203
+ if (args[0] === '-v' || args[0] === '--version') {
1204
+ console.log(`agentaudit ${getVersion()}`);
1205
+ process.exit(0);
1206
+ }
1207
+
1145
1208
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
1146
1209
  banner();
1147
1210
  console.log(` ${c.bold}Commands:${c.reset}`);
@@ -1162,6 +1225,19 @@ async function main() {
1162
1225
  console.log(` agentaudit audit https://github.com/owner/repo`);
1163
1226
  console.log(` agentaudit check fastmcp`);
1164
1227
  console.log();
1228
+ console.log(` ${c.bold}For deep audits,${c.reset} set an LLM API key:`);
1229
+ if (process.platform === 'win32') {
1230
+ console.log(` ${c.dim}PowerShell: $env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
1231
+ console.log(` ${c.dim}CMD: set ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
1232
+ console.log(` ${c.dim}(or use OPENAI_API_KEY instead)${c.reset}`);
1233
+ } else {
1234
+ console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset} ${c.dim}(or OPENAI_API_KEY)${c.reset}`);
1235
+ }
1236
+ console.log();
1237
+ console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed):${c.reset}`);
1238
+ console.log(` ${c.dim}Add to your MCP config:${c.reset}`);
1239
+ console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
1240
+ console.log();
1165
1241
  process.exit(0);
1166
1242
  }
1167
1243
 
package/index.mjs CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  ListToolsRequestSchema,
26
26
  } from '@modelcontextprotocol/sdk/types.js';
27
27
  import fs from 'fs';
28
+ import os from 'os';
28
29
  import path from 'path';
29
30
  import { execSync } from 'child_process';
30
31
  import { fileURLToPath } from 'url';
@@ -124,9 +125,9 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
124
125
  // ── Repo Helpers ────────────────────────────────────────
125
126
 
126
127
  function cloneRepo(sourceUrl) {
127
- const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
128
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
128
129
  try {
129
- execSync(`git clone --depth 1 "${sourceUrl}" "${tmpDir}/repo" 2>/dev/null`, {
130
+ execSync(`git clone --depth 1 "${sourceUrl}" "${tmpDir}/repo"`, {
130
131
  timeout: 30_000, stdio: 'pipe',
131
132
  });
132
133
  return path.join(tmpDir, 'repo');
@@ -136,7 +137,7 @@ function cloneRepo(sourceUrl) {
136
137
  }
137
138
 
138
139
  function cleanupRepo(repoPath) {
139
- try { execSync(`rm -rf "${path.dirname(repoPath)}"`, { stdio: 'pipe' }); } catch {}
140
+ try { fs.rmSync(path.dirname(repoPath), { recursive: true, force: true }); } catch {}
140
141
  }
141
142
 
142
143
  function slugFromUrl(url) {
@@ -176,12 +177,21 @@ function discoverMcpServers() {
176
177
  const allArgs = [cfg.command, ...(cfg.args || [])].filter(Boolean).join(' ');
177
178
  const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
178
179
  const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
180
+ let remoteService = null;
181
+ if (cfg.url) {
182
+ try {
183
+ const hostParts = new URL(cfg.url).hostname.split('.');
184
+ remoteService = hostParts.length === 3 ? hostParts[1] : hostParts[0];
185
+ } catch {}
186
+ }
179
187
  servers.push({
180
188
  name,
181
189
  command: cfg.command || null,
182
190
  args: cfg.args || [],
191
+ url: cfg.url || null,
183
192
  npm_package: npxMatch?.[1] || null,
184
193
  pip_package: pyMatch?.[1] || null,
194
+ remote_service: remoteService,
185
195
  });
186
196
  }
187
197
  results.push({ platform: c.platform, config_path: c.path, status: 'found', server_count: servers.length, servers });
@@ -221,6 +231,28 @@ async function resolveSourceUrl(server) {
221
231
  } catch {}
222
232
  return `https://pypi.org/project/${server.pip_package}/`;
223
233
  }
234
+ // URL-based remote MCP — try npm with common naming patterns
235
+ if (server.remote_service) {
236
+ for (const tryName of [
237
+ `@${server.remote_service}/mcp-server-${server.remote_service}`,
238
+ `${server.remote_service}-mcp`,
239
+ `mcp-server-${server.remote_service}`,
240
+ ]) {
241
+ try {
242
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
243
+ signal: AbortSignal.timeout(3000),
244
+ });
245
+ if (res.ok) {
246
+ const data = await res.json();
247
+ let repoUrl = data.repository?.url;
248
+ if (repoUrl) {
249
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '');
250
+ if (repoUrl.startsWith('http')) return repoUrl;
251
+ }
252
+ }
253
+ } catch {}
254
+ }
255
+ }
224
256
  return null;
225
257
  }
226
258
 
@@ -332,9 +364,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
332
364
  || srv.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
333
365
 
334
366
  text += `### ${srv.name}\n`;
335
- text += `- Command: \`${[srv.command, ...srv.args].join(' ')}\`\n`;
367
+ if (srv.url) {
368
+ text += `- URL: \`${srv.url}\`\n`;
369
+ } else {
370
+ text += `- Command: \`${[srv.command, ...srv.args].filter(Boolean).join(' ')}\`\n`;
371
+ }
336
372
  if (srv.npm_package) text += `- npm: ${srv.npm_package}\n`;
337
373
  if (srv.pip_package) text += `- pip: ${srv.pip_package}\n`;
374
+ if (srv.remote_service) text += `- Service: ${srv.remote_service}\n`;
338
375
 
339
376
  if (doRegistryCheck) {
340
377
  const regData = await checkRegistry(slug);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {