agentaudit 3.3.0 → 3.4.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 +93 -14
  3. package/index.mjs +42 -1
  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
  }
@@ -705,6 +714,48 @@ function serverSlug(server) {
705
714
  return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
706
715
  }
707
716
 
717
+ async function resolveSourceUrl(server) {
718
+ // Already have it
719
+ if (server.sourceUrl) return server.sourceUrl;
720
+
721
+ // Try npm registry
722
+ if (server.npmPackage) {
723
+ try {
724
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npmPackage)}`, {
725
+ signal: AbortSignal.timeout(5000),
726
+ });
727
+ if (res.ok) {
728
+ const data = await res.json();
729
+ let repoUrl = data.repository?.url;
730
+ if (repoUrl) {
731
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
732
+ if (repoUrl.startsWith('http')) return repoUrl;
733
+ }
734
+ }
735
+ } catch {}
736
+ // Fallback: npm page
737
+ return `https://www.npmjs.com/package/${server.npmPackage}`;
738
+ }
739
+
740
+ // Try PyPI
741
+ if (server.pyPackage) {
742
+ try {
743
+ const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pyPackage)}/json`, {
744
+ signal: AbortSignal.timeout(5000),
745
+ });
746
+ if (res.ok) {
747
+ const data = await res.json();
748
+ const urls = data.info?.project_urls || {};
749
+ const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
750
+ if (source && source.startsWith('http')) return source;
751
+ }
752
+ } catch {}
753
+ return `https://pypi.org/project/${server.pyPackage}/`;
754
+ }
755
+
756
+ return null;
757
+ }
758
+
708
759
  async function discoverCommand() {
709
760
  console.log(` ${c.bold}Discovering local MCP servers...${c.reset}`);
710
761
  console.log();
@@ -728,6 +779,7 @@ async function discoverCommand() {
728
779
  let checkedServers = 0;
729
780
  let auditedServers = 0;
730
781
  let unauditedServers = 0;
782
+ const unauditedWithUrls = [];
731
783
 
732
784
  for (const config of configs) {
733
785
  const servers = extractServersFromConfig(config.content);
@@ -779,11 +831,18 @@ async function discoverCommand() {
779
831
  console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
780
832
  } else {
781
833
  unauditedServers++;
834
+ // Resolve source URL
835
+ const resolvedUrl = await resolveSourceUrl(server);
782
836
  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}`);
837
+ if (resolvedUrl) {
838
+ console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: ${c.cyan}agentaudit audit ${resolvedUrl}${c.reset}`);
839
+ unauditedWithUrls.push({ name: server.name, sourceUrl: resolvedUrl });
840
+ } else {
841
+ console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Source URL unknown — check the package's GitHub/npm page${c.reset}`);
842
+ }
784
843
  }
785
844
 
786
- if (server.sourceUrl) {
845
+ if (server.sourceUrl && !server.sourceUrl.includes('npmjs.com')) {
787
846
  console.log(`${pipe} ${c.dim}source: ${server.sourceUrl}${c.reset}`);
788
847
  }
789
848
  }
@@ -800,8 +859,15 @@ async function discoverCommand() {
800
859
  console.log();
801
860
 
802
861
  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}`);
862
+ if (unauditedWithUrls.length > 0) {
863
+ console.log(` ${c.dim}To audit unaudited servers:${c.reset}`);
864
+ for (const { name, sourceUrl } of unauditedWithUrls) {
865
+ console.log(` ${c.cyan}agentaudit audit ${sourceUrl}${c.reset} ${c.dim}(${name})${c.reset}`);
866
+ }
867
+ } else {
868
+ console.log(` ${c.dim}To audit unaudited servers, run:${c.reset}`);
869
+ console.log(` ${c.cyan}agentaudit audit <source-url>${c.reset}`);
870
+ }
805
871
  console.log();
806
872
  }
807
873
  }
@@ -857,15 +923,28 @@ async function auditRepo(url) {
857
923
  const openaiKey = process.env.OPENAI_API_KEY;
858
924
 
859
925
  if (!anthropicKey && !openaiKey) {
860
- // No LLM API key — output the prepared audit for piping or MCP use
926
+ // No LLM API key — clear explanation
927
+ console.log();
928
+ console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
929
+ console.log();
930
+ console.log(` ${c.bold}Option 1: Set an API key${c.reset}`);
931
+ console.log(` Supported keys: ${c.cyan}ANTHROPIC_API_KEY${c.reset} or ${c.cyan}OPENAI_API_KEY${c.reset}`);
932
+ console.log();
933
+ console.log(` ${c.dim}# Linux / macOS:${c.reset}`);
934
+ console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
935
+ console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
936
+ console.log();
937
+ console.log(` ${c.dim}# Windows (PowerShell):${c.reset}`);
938
+ console.log(` ${c.dim}$env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
939
+ console.log(` ${c.dim}$env:OPENAI_API_KEY = "sk-..."${c.reset}`);
861
940
  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}`);
941
+ console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
942
+ console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
943
+ console.log(` ${c.dim}Creates a markdown file you can paste into any LLM (Claude, ChatGPT, etc.)${c.reset}`);
865
944
  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}`);
945
+ console.log(` ${c.bold}Option 3: Use MCP in Claude/Cursor/Windsurf (no API key needed)${c.reset}`);
946
+ console.log(` ${c.dim}Add AgentAudit as MCP server — your editor's agent runs the audit using its own LLM.${c.reset}`);
947
+ console.log(` ${c.dim}Config: { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }${c.reset}`);
869
948
  console.log();
870
949
 
871
950
  // Check if --export flag
package/index.mjs CHANGED
@@ -190,6 +190,40 @@ function discoverMcpServers() {
190
190
  return results;
191
191
  }
192
192
 
193
+ async function resolveSourceUrl(server) {
194
+ if (server.npm_package) {
195
+ try {
196
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npm_package)}`, {
197
+ signal: AbortSignal.timeout(5000),
198
+ });
199
+ if (res.ok) {
200
+ const data = await res.json();
201
+ let repoUrl = data.repository?.url;
202
+ if (repoUrl) {
203
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
204
+ if (repoUrl.startsWith('http')) return repoUrl;
205
+ }
206
+ }
207
+ } catch {}
208
+ return `https://www.npmjs.com/package/${server.npm_package}`;
209
+ }
210
+ if (server.pip_package) {
211
+ try {
212
+ const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pip_package)}/json`, {
213
+ signal: AbortSignal.timeout(5000),
214
+ });
215
+ if (res.ok) {
216
+ const data = await res.json();
217
+ const urls = data.info?.project_urls || {};
218
+ const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
219
+ if (source && source.startsWith('http')) return source;
220
+ }
221
+ } catch {}
222
+ return `https://pypi.org/project/${server.pip_package}/`;
223
+ }
224
+ return null;
225
+ }
226
+
193
227
  async function checkRegistry(slug) {
194
228
  try {
195
229
  const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
@@ -310,8 +344,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
310
344
  text += `- **Registry: ✅ Audited** — Risk ${risk}/100${official}\n`;
311
345
  text += `- Report: ${REGISTRY_URL}/skills/${slug}\n`;
312
346
  } else {
347
+ const sourceUrl = await resolveSourceUrl(srv);
313
348
  text += `- **Registry: ⚠️ Not audited** — no audit report found\n`;
314
- text += `- To audit: call \`audit_package\` with the source URL\n`;
349
+ if (sourceUrl) {
350
+ text += `- Source: ${sourceUrl}\n`;
351
+ text += `- To audit: call \`audit_package\` with source_url \`${sourceUrl}\`\n`;
352
+ } else {
353
+ text += `- Source URL unknown — check the package's GitHub/npm page\n`;
354
+ text += `- To audit: find the source URL, then call \`audit_package\`\n`;
355
+ }
315
356
  }
316
357
  }
317
358
  text += `\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {