agentaudit 3.3.0 → 3.5.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 (4) hide show
  1. package/README.md +42 -12
  2. package/cli.mjs +140 -14
  3. package/index.mjs +79 -2
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -15,16 +15,52 @@ MCP server for agents + standalone CLI for humans.
15
15
 
16
16
  ---
17
17
 
18
- ## Quick Start
18
+ ## Getting Started
19
+
20
+ There are two ways to use AgentAudit:
21
+
22
+ ### Option A: MCP Server in your AI editor (recommended)
23
+
24
+ Add AgentAudit to Claude Desktop, Cursor, or Windsurf. **No API key needed** — your editor's agent runs audits using its own LLM.
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "agentaudit": {
30
+ "command": "npx",
31
+ "args": ["-y", "agentaudit"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Then just ask your agent: *"Check which MCP servers I have installed and audit any unaudited ones."*
38
+
39
+ ### Option B: CLI
19
40
 
20
41
  ```bash
42
+ # Install
43
+ npm install -g agentaudit # or use npx agentaudit <command>
44
+
45
+ # 1. Discover your MCP servers
21
46
  npx agentaudit discover
47
+
48
+ # 2. Audit unaudited packages (needs an LLM API key)
49
+ export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY=sk-...
50
+ npx agentaudit audit https://github.com/owner/repo
51
+
52
+ # 3. (Optional) Register to upload reports to the public registry
53
+ npx agentaudit setup
22
54
  ```
23
55
 
24
- That's it. Finds all MCP servers on your machine and checks them against the security registry.
56
+ > **Note:** The `audit` command requires an LLM API key (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) to analyze code. The `discover`, `scan`, and `check` commands work without one. If you don't have an API key, use `--export` to generate a markdown file you can paste into any LLM, or use AgentAudit as an MCP server (Option A) where no extra key is needed.
57
+
58
+ ### Quick example
25
59
 
26
60
  ```
27
- AgentAudit v3.2.0
61
+ $ npx agentaudit discover
62
+
63
+ AgentAudit v3.3.0
28
64
  Security scanner for AI packages
29
65
 
30
66
  • Scanning Claude Desktop ~/.claude/mcp.json found 2 servers
@@ -32,21 +68,15 @@ AgentAudit v3.2.0
32
68
  ├── fastmcp-demo npm:fastmcp
33
69
  │ SAFE Risk 0 ✔ official https://agentaudit.dev/skills/fastmcp
34
70
  └── my-tool npm:some-mcp-tool
35
- ⚠ not audited Run: agentaudit audit <source-url>
71
+ ⚠ not audited Run: agentaudit audit https://github.com/user/some-mcp-tool
36
72
 
37
- ────────────────────────────────────────────────────────
38
73
  Summary 2 servers across 1 config
39
74
 
40
75
  ✔ 1 audited
41
76
  ⚠ 1 not audited
42
- ```
43
-
44
- ## Install
45
77
 
46
- ```bash
47
- npm install -g agentaudit # global install
48
- # or use directly:
49
- npx agentaudit <command>
78
+ To audit unaudited servers:
79
+ agentaudit audit https://github.com/user/some-mcp-tool (my-tool)
50
80
  ```
51
81
 
52
82
  ---
package/cli.mjs CHANGED
@@ -157,17 +157,26 @@ async function setupCommand() {
157
157
 
158
158
  console.log();
159
159
  console.log(` ${c.bold}Ready!${c.reset} You can now:`);
160
- console.log(` ${c.dim}•${c.reset} Scan packages: ${c.cyan}agentaudit scan <repo-url>${c.reset}`);
161
- console.log(` ${c.dim}•${c.reset} Check registry: ${c.cyan}agentaudit check <name>${c.reset}`);
160
+ console.log(` ${c.dim}•${c.reset} Discover servers: ${c.cyan}agentaudit discover${c.reset}`);
161
+ console.log(` ${c.dim}•${c.reset} Audit packages: ${c.cyan}agentaudit audit <repo-url>${c.reset} ${c.dim}(deep LLM analysis)${c.reset}`);
162
+ console.log(` ${c.dim}•${c.reset} Quick scan: ${c.cyan}agentaudit scan <repo-url>${c.reset} ${c.dim}(regex-based)${c.reset}`);
163
+ console.log(` ${c.dim}•${c.reset} Check registry: ${c.cyan}agentaudit check <name>${c.reset}`);
162
164
  console.log(` ${c.dim}•${c.reset} Submit reports via MCP in Claude/Cursor/Windsurf`);
163
165
  console.log();
164
166
  }
165
167
 
166
168
  // ── Helpers ──────────────────────────────────────────────
167
169
 
170
+ function getVersion() {
171
+ try {
172
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
173
+ return pkg.version || '0.0.0';
174
+ } catch { return '0.0.0'; }
175
+ }
176
+
168
177
  function banner() {
169
178
  console.log();
170
- console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v1.0.0${c.reset}`);
179
+ console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
171
180
  console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
172
181
  console.log();
173
182
  }
@@ -693,6 +702,19 @@ function extractServersFromConfig(config) {
693
702
  const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
694
703
  if (pyMatch) info.pyPackage = pyMatch[1];
695
704
 
705
+ // URL-based MCP server (remote HTTP)
706
+ if (info.url && !info.npmPackage && !info.pyPackage) {
707
+ try {
708
+ const parsed = new URL(info.url);
709
+ // Extract service name from hostname: mcp.supabase.com → supabase
710
+ const hostParts = parsed.hostname.split('.');
711
+ if (hostParts.length >= 2) {
712
+ const serviceName = hostParts.length === 3 ? hostParts[1] : hostParts[0];
713
+ info.remoteService = serviceName;
714
+ }
715
+ } catch {}
716
+ }
717
+
696
718
  result.push(info);
697
719
  }
698
720
  return result;
@@ -705,6 +727,81 @@ function serverSlug(server) {
705
727
  return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
706
728
  }
707
729
 
730
+ async function resolveSourceUrl(server) {
731
+ // Already have it
732
+ if (server.sourceUrl) return server.sourceUrl;
733
+
734
+ // Try npm registry
735
+ if (server.npmPackage) {
736
+ try {
737
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npmPackage)}`, {
738
+ signal: AbortSignal.timeout(5000),
739
+ });
740
+ if (res.ok) {
741
+ const data = await res.json();
742
+ let repoUrl = data.repository?.url;
743
+ if (repoUrl) {
744
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
745
+ if (repoUrl.startsWith('http')) return repoUrl;
746
+ }
747
+ }
748
+ } catch {}
749
+ // Fallback: npm page
750
+ return `https://www.npmjs.com/package/${server.npmPackage}`;
751
+ }
752
+
753
+ // Try PyPI
754
+ if (server.pyPackage) {
755
+ try {
756
+ const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pyPackage)}/json`, {
757
+ signal: AbortSignal.timeout(5000),
758
+ });
759
+ if (res.ok) {
760
+ const data = await res.json();
761
+ const urls = data.info?.project_urls || {};
762
+ const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
763
+ if (source && source.startsWith('http')) return source;
764
+ }
765
+ } catch {}
766
+ return `https://pypi.org/project/${server.pyPackage}/`;
767
+ }
768
+
769
+ // URL-based remote MCP server — try GitHub search by service name
770
+ if (server.remoteService) {
771
+ // Try npm registry with common MCP naming patterns
772
+ for (const tryName of [
773
+ `@${server.remoteService}/mcp-server-${server.remoteService}`,
774
+ `${server.remoteService}-mcp`,
775
+ `mcp-server-${server.remoteService}`,
776
+ server.remoteService,
777
+ ]) {
778
+ try {
779
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
780
+ signal: AbortSignal.timeout(3000),
781
+ });
782
+ if (res.ok) {
783
+ const data = await res.json();
784
+ let repoUrl = data.repository?.url;
785
+ if (repoUrl) {
786
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
787
+ if (repoUrl.startsWith('http')) return repoUrl;
788
+ }
789
+ }
790
+ } catch {}
791
+ }
792
+ }
793
+
794
+ // Last resort: if server has a url, show it as context
795
+ if (server.url) {
796
+ try {
797
+ const parsed = new URL(server.url);
798
+ return `https://github.com/search?q=${encodeURIComponent(parsed.hostname + ' MCP')}&type=repositories`;
799
+ } catch {}
800
+ }
801
+
802
+ return null;
803
+ }
804
+
708
805
  async function discoverCommand() {
709
806
  console.log(` ${c.bold}Discovering local MCP servers...${c.reset}`);
710
807
  console.log();
@@ -728,6 +825,7 @@ async function discoverCommand() {
728
825
  let checkedServers = 0;
729
826
  let auditedServers = 0;
730
827
  let unauditedServers = 0;
828
+ const unauditedWithUrls = [];
731
829
 
732
830
  for (const config of configs) {
733
831
  const servers = extractServersFromConfig(config.content);
@@ -769,6 +867,7 @@ async function discoverCommand() {
769
867
  let sourceLabel = '';
770
868
  if (server.npmPackage) sourceLabel = `${c.dim}npm:${server.npmPackage}${c.reset}`;
771
869
  else if (server.pyPackage) sourceLabel = `${c.dim}pip:${server.pyPackage}${c.reset}`;
870
+ else if (server.url) sourceLabel = `${c.dim}${server.url.length > 60 ? server.url.slice(0, 57) + '...' : server.url}${c.reset}`;
772
871
  else if (server.command) sourceLabel = `${c.dim}${[server.command, ...server.args.slice(0, 2)].join(' ')}${c.reset}`;
773
872
 
774
873
  if (regData) {
@@ -779,11 +878,18 @@ async function discoverCommand() {
779
878
  console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
780
879
  } else {
781
880
  unauditedServers++;
881
+ // Resolve source URL
882
+ const resolvedUrl = await resolveSourceUrl(server);
782
883
  console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
783
- console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: agentaudit audit <source-url>${c.reset}`);
884
+ if (resolvedUrl) {
885
+ console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: ${c.cyan}agentaudit audit ${resolvedUrl}${c.reset}`);
886
+ unauditedWithUrls.push({ name: server.name, sourceUrl: resolvedUrl });
887
+ } else {
888
+ console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Source URL unknown — check the package's GitHub/npm page${c.reset}`);
889
+ }
784
890
  }
785
891
 
786
- if (server.sourceUrl) {
892
+ if (server.sourceUrl && !server.sourceUrl.includes('npmjs.com')) {
787
893
  console.log(`${pipe} ${c.dim}source: ${server.sourceUrl}${c.reset}`);
788
894
  }
789
895
  }
@@ -800,8 +906,15 @@ async function discoverCommand() {
800
906
  console.log();
801
907
 
802
908
  if (unauditedServers > 0) {
803
- console.log(` ${c.dim}To audit unaudited servers, run:${c.reset}`);
804
- console.log(` ${c.cyan}agentaudit scan <github-url>${c.reset}`);
909
+ if (unauditedWithUrls.length > 0) {
910
+ console.log(` ${c.dim}To audit unaudited servers:${c.reset}`);
911
+ for (const { name, sourceUrl } of unauditedWithUrls) {
912
+ console.log(` ${c.cyan}agentaudit audit ${sourceUrl}${c.reset} ${c.dim}(${name})${c.reset}`);
913
+ }
914
+ } else {
915
+ console.log(` ${c.dim}To audit unaudited servers, run:${c.reset}`);
916
+ console.log(` ${c.cyan}agentaudit audit <source-url>${c.reset}`);
917
+ }
805
918
  console.log();
806
919
  }
807
920
  }
@@ -857,15 +970,28 @@ async function auditRepo(url) {
857
970
  const openaiKey = process.env.OPENAI_API_KEY;
858
971
 
859
972
  if (!anthropicKey && !openaiKey) {
860
- // No LLM API key — output the prepared audit for piping or MCP use
973
+ // No LLM API key — clear explanation
974
+ console.log();
975
+ console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
976
+ console.log();
977
+ console.log(` ${c.bold}Option 1: Set an API key${c.reset}`);
978
+ console.log(` Supported keys: ${c.cyan}ANTHROPIC_API_KEY${c.reset} or ${c.cyan}OPENAI_API_KEY${c.reset}`);
979
+ console.log();
980
+ console.log(` ${c.dim}# Linux / macOS:${c.reset}`);
981
+ console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
982
+ console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
983
+ console.log();
984
+ console.log(` ${c.dim}# Windows (PowerShell):${c.reset}`);
985
+ console.log(` ${c.dim}$env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
986
+ console.log(` ${c.dim}$env:OPENAI_API_KEY = "sk-..."${c.reset}`);
861
987
  console.log();
862
- console.log(` ${c.yellow}No LLM API key found.${c.reset} To run the audit automatically, set one of:`);
863
- console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
864
- console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
988
+ console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
989
+ console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
990
+ console.log(` ${c.dim}Creates a markdown file you can paste into any LLM (Claude, ChatGPT, etc.)${c.reset}`);
865
991
  console.log();
866
- console.log(` ${c.bold}Alternatives:${c.reset}`);
867
- console.log(` ${c.dim}1.${c.reset} Use the MCP server in Claude/Cursor — your agent runs the audit automatically`);
868
- console.log(` ${c.dim}2.${c.reset} Export for manual review: ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
992
+ console.log(` ${c.bold}Option 3: Use MCP in Claude/Cursor/Windsurf (no API key needed)${c.reset}`);
993
+ console.log(` ${c.dim}Add AgentAudit as MCP server — your editor's agent runs the audit using its own LLM.${c.reset}`);
994
+ console.log(` ${c.dim}Config: { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }${c.reset}`);
869
995
  console.log();
870
996
 
871
997
  // Check if --export flag
package/index.mjs CHANGED
@@ -176,12 +176,21 @@ function discoverMcpServers() {
176
176
  const allArgs = [cfg.command, ...(cfg.args || [])].filter(Boolean).join(' ');
177
177
  const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
178
178
  const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
179
+ let remoteService = null;
180
+ if (cfg.url) {
181
+ try {
182
+ const hostParts = new URL(cfg.url).hostname.split('.');
183
+ remoteService = hostParts.length === 3 ? hostParts[1] : hostParts[0];
184
+ } catch {}
185
+ }
179
186
  servers.push({
180
187
  name,
181
188
  command: cfg.command || null,
182
189
  args: cfg.args || [],
190
+ url: cfg.url || null,
183
191
  npm_package: npxMatch?.[1] || null,
184
192
  pip_package: pyMatch?.[1] || null,
193
+ remote_service: remoteService,
185
194
  });
186
195
  }
187
196
  results.push({ platform: c.platform, config_path: c.path, status: 'found', server_count: servers.length, servers });
@@ -190,6 +199,62 @@ function discoverMcpServers() {
190
199
  return results;
191
200
  }
192
201
 
202
+ async function resolveSourceUrl(server) {
203
+ if (server.npm_package) {
204
+ try {
205
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npm_package)}`, {
206
+ signal: AbortSignal.timeout(5000),
207
+ });
208
+ if (res.ok) {
209
+ const data = await res.json();
210
+ let repoUrl = data.repository?.url;
211
+ if (repoUrl) {
212
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
213
+ if (repoUrl.startsWith('http')) return repoUrl;
214
+ }
215
+ }
216
+ } catch {}
217
+ return `https://www.npmjs.com/package/${server.npm_package}`;
218
+ }
219
+ if (server.pip_package) {
220
+ try {
221
+ const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pip_package)}/json`, {
222
+ signal: AbortSignal.timeout(5000),
223
+ });
224
+ if (res.ok) {
225
+ const data = await res.json();
226
+ const urls = data.info?.project_urls || {};
227
+ const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
228
+ if (source && source.startsWith('http')) return source;
229
+ }
230
+ } catch {}
231
+ return `https://pypi.org/project/${server.pip_package}/`;
232
+ }
233
+ // URL-based remote MCP — try npm with common naming patterns
234
+ if (server.remote_service) {
235
+ for (const tryName of [
236
+ `@${server.remote_service}/mcp-server-${server.remote_service}`,
237
+ `${server.remote_service}-mcp`,
238
+ `mcp-server-${server.remote_service}`,
239
+ ]) {
240
+ try {
241
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
242
+ signal: AbortSignal.timeout(3000),
243
+ });
244
+ if (res.ok) {
245
+ const data = await res.json();
246
+ let repoUrl = data.repository?.url;
247
+ if (repoUrl) {
248
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '');
249
+ if (repoUrl.startsWith('http')) return repoUrl;
250
+ }
251
+ }
252
+ } catch {}
253
+ }
254
+ }
255
+ return null;
256
+ }
257
+
193
258
  async function checkRegistry(slug) {
194
259
  try {
195
260
  const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
@@ -298,9 +363,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
298
363
  || srv.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
299
364
 
300
365
  text += `### ${srv.name}\n`;
301
- text += `- Command: \`${[srv.command, ...srv.args].join(' ')}\`\n`;
366
+ if (srv.url) {
367
+ text += `- URL: \`${srv.url}\`\n`;
368
+ } else {
369
+ text += `- Command: \`${[srv.command, ...srv.args].filter(Boolean).join(' ')}\`\n`;
370
+ }
302
371
  if (srv.npm_package) text += `- npm: ${srv.npm_package}\n`;
303
372
  if (srv.pip_package) text += `- pip: ${srv.pip_package}\n`;
373
+ if (srv.remote_service) text += `- Service: ${srv.remote_service}\n`;
304
374
 
305
375
  if (doRegistryCheck) {
306
376
  const regData = await checkRegistry(slug);
@@ -310,8 +380,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
310
380
  text += `- **Registry: ✅ Audited** — Risk ${risk}/100${official}\n`;
311
381
  text += `- Report: ${REGISTRY_URL}/skills/${slug}\n`;
312
382
  } else {
383
+ const sourceUrl = await resolveSourceUrl(srv);
313
384
  text += `- **Registry: ⚠️ Not audited** — no audit report found\n`;
314
- text += `- To audit: call \`audit_package\` with the source URL\n`;
385
+ if (sourceUrl) {
386
+ text += `- Source: ${sourceUrl}\n`;
387
+ text += `- To audit: call \`audit_package\` with source_url \`${sourceUrl}\`\n`;
388
+ } else {
389
+ text += `- Source URL unknown — check the package's GitHub/npm page\n`;
390
+ text += `- To audit: find the source URL, then call \`audit_package\`\n`;
391
+ }
315
392
  }
316
393
  }
317
394
  text += `\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.3.0",
3
+ "version": "3.5.0",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {