agentaudit 3.2.0 → 3.3.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/README.md +121 -123
- package/index.mjs +193 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
**Security scanner for AI packages**
|
|
6
6
|
|
|
7
|
-
Scan MCP servers, agent skills, and AI tools for vulnerabilities
|
|
7
|
+
Scan MCP servers, agent skills, and AI tools for vulnerabilities.
|
|
8
|
+
MCP server for agents + standalone CLI for humans.
|
|
8
9
|
|
|
9
10
|
[](https://www.npmjs.com/package/agentaudit)
|
|
10
|
-
[](https://agentaudit.dev)
|
|
11
12
|
[](LICENSE)
|
|
12
13
|
|
|
13
14
|
</div>
|
|
@@ -17,76 +18,100 @@ Scan MCP servers, agent skills, and AI tools for vulnerabilities — from the te
|
|
|
17
18
|
## Quick Start
|
|
18
19
|
|
|
19
20
|
```bash
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
npx agentaudit discover
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That's it. Finds all MCP servers on your machine and checks them against the security registry.
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
```
|
|
27
|
+
AgentAudit v3.2.0
|
|
28
|
+
Security scanner for AI packages
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
agentaudit scan https://github.com/owner/repo
|
|
30
|
+
• Scanning Claude Desktop ~/.claude/mcp.json found 2 servers
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
├── fastmcp-demo npm:fastmcp
|
|
33
|
+
│ SAFE Risk 0 ✔ official https://agentaudit.dev/skills/fastmcp
|
|
34
|
+
└── my-tool npm:some-mcp-tool
|
|
35
|
+
⚠ not audited Run: agentaudit audit <source-url>
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
────────────────────────────────────────────────────────
|
|
38
|
+
Summary 2 servers across 1 config
|
|
39
|
+
|
|
40
|
+
✔ 1 audited
|
|
41
|
+
⚠ 1 not audited
|
|
34
42
|
```
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
## Install
|
|
37
45
|
|
|
38
46
|
```bash
|
|
39
|
-
|
|
47
|
+
npm install -g agentaudit # global install
|
|
48
|
+
# or use directly:
|
|
49
|
+
npx agentaudit <command>
|
|
40
50
|
```
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
---
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
◉ google-workspace-mcp https://github.com/taylorwilsdon/google_workspace_mcp
|
|
46
|
-
│ Python mcp-server 31 files scanned in 1.0s
|
|
47
|
-
│
|
|
48
|
-
├── tool drive_service ✔ ok
|
|
49
|
-
├── tool docs_service ✔ ok
|
|
50
|
-
├── tool start_google_auth ✔ ok
|
|
51
|
-
└── tool set_enabled_tools ✔ ok
|
|
52
|
-
│
|
|
53
|
-
│ Findings (2) static analysis — may include false positives
|
|
54
|
-
├── ● MEDIUM Potential hardcoded secret
|
|
55
|
-
│ .env.oauth21:9 SECRET="your-google-client-secret"
|
|
56
|
-
└── ● MEDIUM Potential path traversal
|
|
57
|
-
auth/credential_store.py:123
|
|
58
|
-
│
|
|
59
|
-
└── registry LOW Risk 10 https://agentaudit.dev/skills/google-workspace-mcp
|
|
60
|
-
```
|
|
54
|
+
## Commands
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
- 🟡 Unsafe YAML/pickle deserialization
|
|
70
|
-
- 🔵 Wildcard CORS
|
|
71
|
-
- 🔵 Undisclosed telemetry
|
|
56
|
+
| Command | What it does | Speed |
|
|
57
|
+
|---------|-------------|-------|
|
|
58
|
+
| `discover` | Find local MCP servers + check registry | ⚡ instant |
|
|
59
|
+
| `check <name>` | Look up a package in the registry | ⚡ instant |
|
|
60
|
+
| `scan <url>` | Quick static analysis (regex-based, local) | 🔵 ~2s |
|
|
61
|
+
| `audit <url>` | **Deep LLM-powered security audit** | 🔴 ~30s |
|
|
62
|
+
| `setup` | Register + configure API key | interactive |
|
|
72
63
|
|
|
73
|
-
|
|
64
|
+
### `scan` vs `audit`
|
|
65
|
+
|
|
66
|
+
| | `scan` | `audit` |
|
|
67
|
+
|--|--------|---------|
|
|
68
|
+
| **How** | Regex pattern matching | LLM 3-pass: UNDERSTAND → DETECT → CLASSIFY |
|
|
69
|
+
| **Speed** | ~2 seconds | ~30 seconds |
|
|
70
|
+
| **Depth** | Surface-level patterns | Semantic code understanding |
|
|
71
|
+
| **Needs LLM API key** | No | Yes (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) |
|
|
72
|
+
| **Uploads to registry** | No | Yes (with `agentaudit setup`) |
|
|
73
|
+
|
|
74
|
+
### Examples
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Discover all MCP servers on your machine
|
|
78
|
+
npx agentaudit discover
|
|
79
|
+
|
|
80
|
+
# Quick static scan
|
|
81
|
+
npx agentaudit scan https://github.com/owner/repo
|
|
82
|
+
|
|
83
|
+
# Deep LLM-powered audit
|
|
84
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
85
|
+
npx agentaudit audit https://github.com/owner/repo
|
|
86
|
+
|
|
87
|
+
# Export code + audit prompt for manual LLM review
|
|
88
|
+
npx agentaudit audit https://github.com/owner/repo --export
|
|
89
|
+
|
|
90
|
+
# Registry lookup
|
|
91
|
+
npx agentaudit check fastmcp
|
|
92
|
+
|
|
93
|
+
# Register for an API key (free)
|
|
94
|
+
npx agentaudit setup
|
|
95
|
+
```
|
|
74
96
|
|
|
75
97
|
---
|
|
76
98
|
|
|
77
99
|
## MCP Server
|
|
78
100
|
|
|
79
|
-
|
|
101
|
+
Add AgentAudit to your AI editor. Your agent gets 4 tools:
|
|
80
102
|
|
|
81
|
-
| Tool | Description |
|
|
82
|
-
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
103
|
+
| MCP Tool | Description |
|
|
104
|
+
|----------|-------------|
|
|
105
|
+
| `discover_servers` | Find all locally installed MCP servers, check registry status |
|
|
106
|
+
| `audit_package` | Clone a repo → return source code + audit prompt → you analyze → `submit_report` |
|
|
107
|
+
| `submit_report` | Upload your audit report to [agentaudit.dev](https://agentaudit.dev) |
|
|
108
|
+
| `check_package` | Quick registry lookup for a package |
|
|
86
109
|
|
|
87
|
-
###
|
|
110
|
+
### Setup
|
|
88
111
|
|
|
89
|
-
|
|
112
|
+
One-line config — works with `npx`, no manual clone needed:
|
|
113
|
+
|
|
114
|
+
**Claude Desktop** (`~/.claude/mcp.json`):
|
|
90
115
|
```json
|
|
91
116
|
{
|
|
92
117
|
"mcpServers": {
|
|
@@ -98,9 +123,7 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
|
|
|
98
123
|
}
|
|
99
124
|
```
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`.cursor/mcp.json`:
|
|
126
|
+
**Cursor** (`.cursor/mcp.json`):
|
|
104
127
|
```json
|
|
105
128
|
{
|
|
106
129
|
"mcpServers": {
|
|
@@ -112,9 +135,7 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
|
|
|
112
135
|
}
|
|
113
136
|
```
|
|
114
137
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
`~/.codeium/windsurf/mcp_config.json`:
|
|
138
|
+
**Windsurf** (`~/.codeium/windsurf/mcp_config.json`):
|
|
118
139
|
```json
|
|
119
140
|
{
|
|
120
141
|
"mcpServers": {
|
|
@@ -126,100 +147,77 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
|
|
|
126
147
|
}
|
|
127
148
|
```
|
|
128
149
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
### How the MCP audit works
|
|
150
|
+
### How an audit works via MCP
|
|
132
151
|
|
|
133
152
|
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Agent
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
## Setup & Authentication
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
agentaudit setup
|
|
153
|
+
You: "Audit the blender-mcp server"
|
|
154
|
+
↓
|
|
155
|
+
Agent calls discover_servers → finds blender-mcp
|
|
156
|
+
↓
|
|
157
|
+
Agent calls check_package("blender-mcp") → not in registry
|
|
158
|
+
↓
|
|
159
|
+
Agent calls audit_package("https://github.com/user/blender-mcp")
|
|
160
|
+
↓
|
|
161
|
+
MCP server clones repo, returns source code + audit methodology
|
|
162
|
+
↓
|
|
163
|
+
Agent's LLM analyzes code (3-pass: UNDERSTAND → DETECT → CLASSIFY)
|
|
164
|
+
↓
|
|
165
|
+
Agent calls submit_report(findings_json)
|
|
166
|
+
↓
|
|
167
|
+
Report published at agentaudit.dev/skills/blender-mcp
|
|
153
168
|
```
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
1. **Register new agent** (free) → API key created automatically
|
|
157
|
-
2. **Enter existing API key** → if you already have one
|
|
170
|
+
### Authentication
|
|
158
171
|
|
|
159
|
-
|
|
172
|
+
Run `npx agentaudit setup` once. Both CLI and MCP server find credentials automatically:
|
|
160
173
|
|
|
161
|
-
The MCP server finds credentials automatically from:
|
|
162
174
|
1. `AGENTAUDIT_API_KEY` environment variable
|
|
163
|
-
2. `~/.config/agentaudit/credentials.json`
|
|
175
|
+
2. `~/.config/agentaudit/credentials.json` (created by `setup`)
|
|
164
176
|
|
|
165
|
-
|
|
177
|
+
**`discover`, `scan`, and `check` work without a key.** Only `audit`/`submit_report` need one.
|
|
166
178
|
|
|
167
179
|
---
|
|
168
180
|
|
|
169
|
-
##
|
|
170
|
-
|
|
171
|
-
```
|
|
172
|
-
agentaudit discover Find local MCP servers + check registry
|
|
173
|
-
agentaudit scan <url> [url...] Quick static scan (regex, ~2s)
|
|
174
|
-
agentaudit audit <url> [url...] Deep LLM-powered audit (~30s)
|
|
175
|
-
agentaudit check <name> Look up package in registry
|
|
176
|
-
agentaudit setup Register + configure API key
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### `scan` vs `audit`
|
|
180
|
-
|
|
181
|
-
| | `scan` | `audit` |
|
|
182
|
-
|--|--------|---------|
|
|
183
|
-
| **How** | Regex-based static analysis | LLM 3-pass analysis (UNDERSTAND → DETECT → CLASSIFY) |
|
|
184
|
-
| **Speed** | ~2 seconds | ~30 seconds |
|
|
185
|
-
| **Depth** | Pattern matching | Semantic code understanding |
|
|
186
|
-
| **Needs API key** | No | Yes (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) |
|
|
187
|
-
| **Upload to registry** | No | Yes (with `agentaudit setup`) |
|
|
188
|
-
|
|
189
|
-
Use `scan` for quick checks, `audit` for thorough analysis.
|
|
181
|
+
## What it detects
|
|
190
182
|
|
|
191
|
-
###
|
|
183
|
+
### Static scan (`scan`)
|
|
192
184
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
- 🔴 Prompt injection & tool poisoning
|
|
186
|
+
- 🔴 Shell command injection
|
|
187
|
+
- 🔴 SQL injection
|
|
188
|
+
- 🔴 Unsafe deserialization (pickle, YAML)
|
|
189
|
+
- 🟡 Hardcoded secrets
|
|
190
|
+
- 🟡 SSL/TLS verification disabled
|
|
191
|
+
- 🟡 Path traversal
|
|
192
|
+
- 🔵 Wildcard CORS
|
|
193
|
+
- 🔵 Undisclosed telemetry
|
|
196
194
|
|
|
197
|
-
|
|
198
|
-
agentaudit scan https://github.com/jlowin/fastmcp
|
|
195
|
+
### Deep audit (`audit` / MCP `audit_package`)
|
|
199
196
|
|
|
200
|
-
|
|
201
|
-
agentaudit audit https://github.com/jlowin/fastmcp
|
|
197
|
+
Everything above, plus:
|
|
202
198
|
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
- 🔴 Multi-file attack chains (credential harvest → exfiltration)
|
|
200
|
+
- 🔴 Agent manipulation (impersonation, capability escalation, jailbreaks)
|
|
201
|
+
- 🔴 MCP-specific: tool description injection, resource traversal, unpinned npx
|
|
202
|
+
- 🟡 Persistence mechanisms (crontab, shell RC, git hooks, systemd)
|
|
203
|
+
- 🟡 Obfuscation (base64 exec, zero-width chars, ANSI escapes)
|
|
204
|
+
- 🟡 Context pollution & indirect prompt injection
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
agentaudit check mongodb-mcp-server
|
|
208
|
-
```
|
|
206
|
+
50+ detection patterns across 8 categories. [Full pattern list →](https://github.com/starbuck100/agentaudit-skill)
|
|
209
207
|
|
|
210
208
|
---
|
|
211
209
|
|
|
212
210
|
## Requirements
|
|
213
211
|
|
|
214
212
|
- **Node.js 18+**
|
|
215
|
-
- **Git** (for cloning repos
|
|
213
|
+
- **Git** (for cloning repos)
|
|
216
214
|
|
|
217
215
|
---
|
|
218
216
|
|
|
219
217
|
## Related
|
|
220
218
|
|
|
221
|
-
- [agentaudit.dev](https://agentaudit.dev) — Trust registry
|
|
222
|
-
- [agentaudit-skill](https://github.com/starbuck100/agentaudit-skill) — Full agent skill with gate scripts, detection patterns & peer review
|
|
219
|
+
- **[agentaudit.dev](https://agentaudit.dev)** — Trust registry with 400+ audit reports
|
|
220
|
+
- **[agentaudit-skill](https://github.com/starbuck100/agentaudit-skill)** — Full agent skill with gate scripts, detection patterns & peer review system
|
|
223
221
|
|
|
224
222
|
---
|
|
225
223
|
|
package/index.mjs
CHANGED
|
@@ -2,25 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AgentAudit MCP Server
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Security audit capabilities via Model Context Protocol.
|
|
6
6
|
*
|
|
7
7
|
* Tools:
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
8
|
+
* - discover_servers Find locally installed MCP servers + check registry status
|
|
9
|
+
* - audit_package Clone a repo, return source code + audit prompt for LLM analysis
|
|
10
|
+
* - submit_report Upload a completed audit report to agentaudit.dev
|
|
11
|
+
* - check_package Look up a package in the AgentAudit registry
|
|
11
12
|
*
|
|
12
13
|
* Usage:
|
|
13
|
-
*
|
|
14
|
+
* npx agentaudit (starts MCP server via stdio)
|
|
15
|
+
* node index.mjs (same)
|
|
14
16
|
*
|
|
15
|
-
* Configure in Claude/Cursor/
|
|
16
|
-
* {
|
|
17
|
-
* "mcpServers": {
|
|
18
|
-
* "agentaudit": {
|
|
19
|
-
* "command": "node",
|
|
20
|
-
* "args": ["path/to/agentaudit/mcp-server/index.mjs"]
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
* }
|
|
17
|
+
* Configure in Claude/Cursor/Windsurf:
|
|
18
|
+
* { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }
|
|
24
19
|
*/
|
|
25
20
|
|
|
26
21
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -37,8 +32,8 @@ import { fileURLToPath } from 'url';
|
|
|
37
32
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
33
|
const SKILL_DIR = path.resolve(__dirname);
|
|
39
34
|
const REGISTRY_URL = 'https://agentaudit.dev';
|
|
40
|
-
const MAX_FILE_SIZE = 50_000;
|
|
41
|
-
const MAX_TOTAL_SIZE = 300_000;
|
|
35
|
+
const MAX_FILE_SIZE = 50_000;
|
|
36
|
+
const MAX_TOTAL_SIZE = 300_000;
|
|
42
37
|
const SKIP_DIRS = new Set([
|
|
43
38
|
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
|
|
44
39
|
'.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
|
|
@@ -61,7 +56,7 @@ const PRIORITY_FILES = [
|
|
|
61
56
|
'Makefile', 'Dockerfile', 'docker-compose.yml',
|
|
62
57
|
];
|
|
63
58
|
|
|
64
|
-
// ──
|
|
59
|
+
// ── Credentials ─────────────────────────────────────────
|
|
65
60
|
|
|
66
61
|
function loadApiKey() {
|
|
67
62
|
if (process.env.AGENTAUDIT_API_KEY) return process.env.AGENTAUDIT_API_KEY;
|
|
@@ -84,39 +79,32 @@ function loadApiKey() {
|
|
|
84
79
|
|
|
85
80
|
function loadAuditPrompt() {
|
|
86
81
|
const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
|
|
87
|
-
if (fs.existsSync(promptPath))
|
|
88
|
-
return fs.readFileSync(promptPath, 'utf8');
|
|
89
|
-
}
|
|
82
|
+
if (fs.existsSync(promptPath)) return fs.readFileSync(promptPath, 'utf8');
|
|
90
83
|
return 'ERROR: audit-prompt.md not found at ' + promptPath;
|
|
91
84
|
}
|
|
92
85
|
|
|
86
|
+
// ── File Collection ─────────────────────────────────────
|
|
87
|
+
|
|
93
88
|
function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
|
|
94
89
|
if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
|
|
95
|
-
|
|
96
90
|
let entries;
|
|
97
91
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
98
92
|
catch { return collected; }
|
|
99
|
-
|
|
100
|
-
// Sort: priority files first
|
|
101
93
|
entries.sort((a, b) => {
|
|
102
94
|
const aP = PRIORITY_FILES.includes(a.name) ? 0 : 1;
|
|
103
95
|
const bP = PRIORITY_FILES.includes(b.name) ? 0 : 1;
|
|
104
96
|
return aP - bP || a.name.localeCompare(b.name);
|
|
105
97
|
});
|
|
106
|
-
|
|
107
98
|
for (const entry of entries) {
|
|
108
99
|
if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
|
|
109
|
-
|
|
110
100
|
const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
111
101
|
const fullPath = path.join(dir, entry.name);
|
|
112
|
-
|
|
113
102
|
if (entry.isDirectory()) {
|
|
114
103
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
115
104
|
collectFiles(fullPath, relPath, collected, totalSize);
|
|
116
105
|
} else {
|
|
117
106
|
const ext = path.extname(entry.name).toLowerCase();
|
|
118
107
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
119
|
-
|
|
120
108
|
try {
|
|
121
109
|
const stat = fs.statSync(fullPath);
|
|
122
110
|
if (stat.size > MAX_FILE_SIZE) {
|
|
@@ -124,24 +112,22 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
|
124
112
|
continue;
|
|
125
113
|
}
|
|
126
114
|
if (stat.size === 0) continue;
|
|
127
|
-
|
|
128
115
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
129
116
|
totalSize.bytes += content.length;
|
|
130
117
|
collected.push({ path: relPath, content });
|
|
131
|
-
} catch {
|
|
132
|
-
// Binary or unreadable — skip
|
|
133
|
-
}
|
|
118
|
+
} catch {}
|
|
134
119
|
}
|
|
135
120
|
}
|
|
136
121
|
return collected;
|
|
137
122
|
}
|
|
138
123
|
|
|
124
|
+
// ── Repo Helpers ────────────────────────────────────────
|
|
125
|
+
|
|
139
126
|
function cloneRepo(sourceUrl) {
|
|
140
127
|
const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
|
|
141
128
|
try {
|
|
142
129
|
execSync(`git clone --depth 1 "${sourceUrl}" "${tmpDir}/repo" 2>/dev/null`, {
|
|
143
|
-
timeout: 30_000,
|
|
144
|
-
stdio: 'pipe',
|
|
130
|
+
timeout: 30_000, stdio: 'pipe',
|
|
145
131
|
});
|
|
146
132
|
return path.join(tmpDir, 'repo');
|
|
147
133
|
} catch (err) {
|
|
@@ -150,30 +136,95 @@ function cloneRepo(sourceUrl) {
|
|
|
150
136
|
}
|
|
151
137
|
|
|
152
138
|
function cleanupRepo(repoPath) {
|
|
153
|
-
try {
|
|
154
|
-
execSync(`rm -rf "${path.dirname(repoPath)}"`, { stdio: 'pipe' });
|
|
155
|
-
} catch {}
|
|
139
|
+
try { execSync(`rm -rf "${path.dirname(repoPath)}"`, { stdio: 'pipe' }); } catch {}
|
|
156
140
|
}
|
|
157
141
|
|
|
158
142
|
function slugFromUrl(url) {
|
|
159
|
-
// https://github.com/owner/repo → owner-repo or just repo
|
|
160
143
|
const match = url.match(/github\.com\/([^/]+)\/([^/.\s]+)/);
|
|
161
144
|
if (match) return match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
162
145
|
return url.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60);
|
|
163
146
|
}
|
|
164
147
|
|
|
148
|
+
// ── Discover local MCP configs ──────────────────────────
|
|
149
|
+
|
|
150
|
+
function discoverMcpServers() {
|
|
151
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
152
|
+
const candidates = [
|
|
153
|
+
{ platform: 'Claude Desktop', path: path.join(home, '.claude', 'mcp.json') },
|
|
154
|
+
{ platform: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
|
|
155
|
+
{ platform: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
|
|
156
|
+
{ platform: 'Claude Desktop', path: path.join(home, '.config', 'claude', 'claude_desktop_config.json') },
|
|
157
|
+
{ platform: 'Cursor', path: path.join(home, '.cursor', 'mcp.json') },
|
|
158
|
+
{ platform: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
|
|
159
|
+
{ platform: 'VS Code', path: path.join(home, '.vscode', 'mcp.json') },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const results = [];
|
|
163
|
+
|
|
164
|
+
for (const c of candidates) {
|
|
165
|
+
if (!fs.existsSync(c.path)) {
|
|
166
|
+
results.push({ platform: c.platform, config_path: c.path, status: 'not found', servers: [] });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
let content;
|
|
170
|
+
try { content = JSON.parse(fs.readFileSync(c.path, 'utf8')); }
|
|
171
|
+
catch { results.push({ platform: c.platform, config_path: c.path, status: 'parse error', servers: [] }); continue; }
|
|
172
|
+
|
|
173
|
+
const serverMap = content.mcpServers || content.servers || {};
|
|
174
|
+
const servers = [];
|
|
175
|
+
for (const [name, cfg] of Object.entries(serverMap)) {
|
|
176
|
+
const allArgs = [cfg.command, ...(cfg.args || [])].filter(Boolean).join(' ');
|
|
177
|
+
const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
|
|
178
|
+
const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
|
|
179
|
+
servers.push({
|
|
180
|
+
name,
|
|
181
|
+
command: cfg.command || null,
|
|
182
|
+
args: cfg.args || [],
|
|
183
|
+
npm_package: npxMatch?.[1] || null,
|
|
184
|
+
pip_package: pyMatch?.[1] || null,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
results.push({ platform: c.platform, config_path: c.path, status: 'found', server_count: servers.length, servers });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function checkRegistry(slug) {
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
|
|
196
|
+
signal: AbortSignal.timeout(5000),
|
|
197
|
+
});
|
|
198
|
+
if (res.ok) return await res.json();
|
|
199
|
+
} catch {}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
165
203
|
// ── MCP Server ───────────────────────────────────────────
|
|
166
204
|
|
|
167
205
|
const server = new Server(
|
|
168
|
-
{ name: 'agentaudit', version: '
|
|
206
|
+
{ name: 'agentaudit', version: '3.2.0' },
|
|
169
207
|
{ capabilities: { tools: {} } }
|
|
170
208
|
);
|
|
171
209
|
|
|
172
210
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
173
211
|
tools: [
|
|
212
|
+
{
|
|
213
|
+
name: 'discover_servers',
|
|
214
|
+
description: 'Find all locally installed MCP servers by scanning config files (Claude Desktop, Cursor, Windsurf, VS Code). Returns the list of configured servers with their names, commands, and package sources. Use this to see what MCP servers are installed, then check each against the registry with check_package, or audit them with audit_package.',
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
check_registry: {
|
|
219
|
+
type: 'boolean',
|
|
220
|
+
description: 'If true, also check each discovered server against the AgentAudit registry (default: true)',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
174
225
|
{
|
|
175
226
|
name: 'audit_package',
|
|
176
|
-
description: '
|
|
227
|
+
description: 'Deep security audit: clone a repository and prepare it for LLM-powered analysis. Returns the source code and a 3-pass audit methodology (UNDERSTAND → DETECT → CLASSIFY). You (the agent) then analyze the code following the instructions and call submit_report with your findings. This is a DEEP audit — use check_package first for a quick registry lookup.',
|
|
177
228
|
inputSchema: {
|
|
178
229
|
type: 'object',
|
|
179
230
|
properties: {
|
|
@@ -187,13 +238,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
187
238
|
},
|
|
188
239
|
{
|
|
189
240
|
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.',
|
|
241
|
+
description: 'Submit a completed security audit report to the AgentAudit registry (agentaudit.dev). Call this after you have analyzed the code from audit_package. The report becomes publicly available and helps other agents make install decisions.',
|
|
191
242
|
inputSchema: {
|
|
192
243
|
type: 'object',
|
|
193
244
|
properties: {
|
|
194
245
|
report: {
|
|
195
246
|
type: 'object',
|
|
196
|
-
description: 'The audit report JSON object.
|
|
247
|
+
description: 'The audit report JSON object. Required fields: skill_slug, source_url, risk_score (0-100), result (safe|caution|unsafe), findings (array), findings_count, max_severity, package_type.',
|
|
197
248
|
},
|
|
198
249
|
},
|
|
199
250
|
required: ['report'],
|
|
@@ -201,7 +252,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
201
252
|
},
|
|
202
253
|
{
|
|
203
254
|
name: 'check_package',
|
|
204
|
-
description: '
|
|
255
|
+
description: 'Quick registry lookup: check if a package has already been audited on agentaudit.dev. Returns the latest audit results (risk score, findings, official status) if available. Use this before audit_package to avoid duplicate work.',
|
|
205
256
|
inputSchema: {
|
|
206
257
|
type: 'object',
|
|
207
258
|
properties: {
|
|
@@ -218,39 +269,99 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
218
269
|
|
|
219
270
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
220
271
|
const { name, arguments: args } = request.params;
|
|
221
|
-
|
|
272
|
+
|
|
222
273
|
switch (name) {
|
|
274
|
+
|
|
275
|
+
// ── discover_servers ──────────────────────────────────
|
|
276
|
+
case 'discover_servers': {
|
|
277
|
+
const doRegistryCheck = args.check_registry !== false;
|
|
278
|
+
const configs = discoverMcpServers();
|
|
279
|
+
const foundConfigs = configs.filter(c => c.status === 'found');
|
|
280
|
+
const allServers = foundConfigs.flatMap(c => c.servers.map(s => ({ ...s, platform: c.platform })));
|
|
281
|
+
|
|
282
|
+
let text = `# Discovered MCP Servers\n\n`;
|
|
283
|
+
text += `Scanned ${configs.length} config locations. Found ${foundConfigs.length} config(s) with ${allServers.length} server(s).\n\n`;
|
|
284
|
+
|
|
285
|
+
for (const config of configs) {
|
|
286
|
+
if (config.status === 'not found') continue;
|
|
287
|
+
text += `## ${config.platform}\n`;
|
|
288
|
+
text += `Config: \`${config.config_path}\`\n\n`;
|
|
289
|
+
|
|
290
|
+
if (config.servers.length === 0) {
|
|
291
|
+
text += `No servers configured.\n\n`;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const srv of config.servers) {
|
|
296
|
+
const slug = srv.npm_package?.replace(/^@/, '').replace(/\//g, '-')
|
|
297
|
+
|| srv.pip_package?.replace(/[^a-z0-9-]/gi, '-')
|
|
298
|
+
|| srv.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
299
|
+
|
|
300
|
+
text += `### ${srv.name}\n`;
|
|
301
|
+
text += `- Command: \`${[srv.command, ...srv.args].join(' ')}\`\n`;
|
|
302
|
+
if (srv.npm_package) text += `- npm: ${srv.npm_package}\n`;
|
|
303
|
+
if (srv.pip_package) text += `- pip: ${srv.pip_package}\n`;
|
|
304
|
+
|
|
305
|
+
if (doRegistryCheck) {
|
|
306
|
+
const regData = await checkRegistry(slug);
|
|
307
|
+
if (regData) {
|
|
308
|
+
const risk = regData.risk_score ?? regData.latest_risk_score ?? 0;
|
|
309
|
+
const official = regData.has_official_audit ? ' (official)' : '';
|
|
310
|
+
text += `- **Registry: ✅ Audited** — Risk ${risk}/100${official}\n`;
|
|
311
|
+
text += `- Report: ${REGISTRY_URL}/skills/${slug}\n`;
|
|
312
|
+
} else {
|
|
313
|
+
text += `- **Registry: ⚠️ Not audited** — no audit report found\n`;
|
|
314
|
+
text += `- To audit: call \`audit_package\` with the source URL\n`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
text += `\n`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (allServers.length === 0) {
|
|
322
|
+
text += `No MCP servers found. Config locations searched:\n`;
|
|
323
|
+
text += `- Claude Desktop: ~/.claude/mcp.json\n`;
|
|
324
|
+
text += `- Cursor: ~/.cursor/mcp.json\n`;
|
|
325
|
+
text += `- Windsurf: ~/.codeium/windsurf/mcp_config.json\n`;
|
|
326
|
+
text += `- VS Code: ~/.vscode/mcp.json\n`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { content: [{ type: 'text', text }] };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── audit_package ─────────────────────────────────────
|
|
223
333
|
case 'audit_package': {
|
|
224
334
|
const { source_url } = args;
|
|
225
335
|
if (!source_url || !source_url.startsWith('http')) {
|
|
226
336
|
return { content: [{ type: 'text', text: 'Error: source_url must be a valid HTTP(S) URL' }] };
|
|
227
337
|
}
|
|
228
|
-
|
|
338
|
+
|
|
229
339
|
let repoPath;
|
|
230
340
|
try {
|
|
231
341
|
repoPath = cloneRepo(source_url);
|
|
232
342
|
const files = collectFiles(repoPath);
|
|
233
343
|
const slug = slugFromUrl(source_url);
|
|
234
344
|
const auditPrompt = loadAuditPrompt();
|
|
235
|
-
|
|
236
|
-
// Build the response
|
|
345
|
+
|
|
237
346
|
let codeBlock = '';
|
|
238
347
|
for (const file of files) {
|
|
239
348
|
codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
|
|
240
349
|
}
|
|
241
|
-
|
|
350
|
+
|
|
242
351
|
const response = [
|
|
243
|
-
`# Security Audit
|
|
352
|
+
`# Security Audit: ${slug}`,
|
|
244
353
|
``,
|
|
245
|
-
`**Package:** ${slug}`,
|
|
246
354
|
`**Source:** ${source_url}`,
|
|
247
355
|
`**Files collected:** ${files.length}`,
|
|
248
356
|
``,
|
|
249
|
-
`##
|
|
357
|
+
`## Your Task`,
|
|
358
|
+
``,
|
|
359
|
+
`1. Analyze the source code below using the 3-pass audit methodology`,
|
|
360
|
+
`2. Call \`submit_report\` with your findings as JSON`,
|
|
250
361
|
``,
|
|
251
|
-
|
|
362
|
+
`## Report Format`,
|
|
252
363
|
``,
|
|
253
|
-
`
|
|
364
|
+
`Your report JSON must include:`,
|
|
254
365
|
'```json',
|
|
255
366
|
`{`,
|
|
256
367
|
` "skill_slug": "${slug}",`,
|
|
@@ -285,7 +396,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
285
396
|
``,
|
|
286
397
|
codeBlock,
|
|
287
398
|
].join('\n');
|
|
288
|
-
|
|
399
|
+
|
|
289
400
|
return { content: [{ type: 'text', text: response }] };
|
|
290
401
|
} catch (err) {
|
|
291
402
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
@@ -293,27 +404,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
293
404
|
if (repoPath) cleanupRepo(repoPath);
|
|
294
405
|
}
|
|
295
406
|
}
|
|
296
|
-
|
|
407
|
+
|
|
408
|
+
// ── submit_report ─────────────────────────────────────
|
|
297
409
|
case 'submit_report': {
|
|
298
410
|
const { report } = args;
|
|
299
411
|
if (!report || typeof report !== 'object') {
|
|
300
412
|
return { content: [{ type: 'text', text: 'Error: report must be a JSON object' }] };
|
|
301
413
|
}
|
|
302
|
-
|
|
414
|
+
|
|
303
415
|
const apiKey = loadApiKey();
|
|
304
416
|
if (!apiKey) {
|
|
305
|
-
return { content: [{ type: 'text', text: 'Error: No API key configured.
|
|
417
|
+
return { content: [{ type: 'text', text: 'Error: No API key configured. Run `npx agentaudit setup` or set AGENTAUDIT_API_KEY.' }] };
|
|
306
418
|
}
|
|
307
|
-
|
|
308
|
-
// Validate required fields
|
|
419
|
+
|
|
309
420
|
const required = ['skill_slug', 'source_url', 'risk_score', 'result'];
|
|
310
421
|
for (const field of required) {
|
|
311
422
|
if (report[field] == null) {
|
|
312
423
|
return { content: [{ type: 'text', text: `Error: Missing required field "${field}" in report` }] };
|
|
313
424
|
}
|
|
314
425
|
}
|
|
315
|
-
|
|
316
|
-
// Auto-fix findings
|
|
426
|
+
|
|
317
427
|
if (!Array.isArray(report.findings)) report.findings = [];
|
|
318
428
|
report.findings_count = report.findings.length;
|
|
319
429
|
if (!report.max_severity) {
|
|
@@ -324,7 +434,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
324
434
|
return fi < mi ? f.severity : max;
|
|
325
435
|
}, 'none');
|
|
326
436
|
}
|
|
327
|
-
|
|
437
|
+
|
|
328
438
|
try {
|
|
329
439
|
const res = await fetch(`${REGISTRY_URL}/api/reports`, {
|
|
330
440
|
method: 'POST',
|
|
@@ -335,13 +445,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
335
445
|
body: JSON.stringify(report),
|
|
336
446
|
signal: AbortSignal.timeout(60_000),
|
|
337
447
|
});
|
|
338
|
-
|
|
448
|
+
|
|
339
449
|
const body = await res.text();
|
|
340
450
|
let data;
|
|
341
451
|
try { data = JSON.parse(body); } catch { data = { raw: body }; }
|
|
342
|
-
|
|
452
|
+
|
|
343
453
|
if (res.ok) {
|
|
344
|
-
return { content: [{ type: 'text', text:
|
|
454
|
+
return { content: [{ type: 'text', text: `✅ Report submitted!\n\nReport ID: ${data.report_id || 'unknown'}\nURL: ${REGISTRY_URL}/skills/${report.skill_slug}\nRisk: ${report.risk_score}/100 (${report.result})\nFindings: ${report.findings_count}` }] };
|
|
345
455
|
} else {
|
|
346
456
|
return { content: [{ type: 'text', text: `Upload failed (HTTP ${res.status}): ${JSON.stringify(data, null, 2)}` }] };
|
|
347
457
|
}
|
|
@@ -349,31 +459,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
349
459
|
return { content: [{ type: 'text', text: `Upload error: ${err.message}` }] };
|
|
350
460
|
}
|
|
351
461
|
}
|
|
352
|
-
|
|
462
|
+
|
|
463
|
+
// ── check_package ─────────────────────────────────────
|
|
353
464
|
case 'check_package': {
|
|
354
465
|
const { package_name } = args;
|
|
355
466
|
if (!package_name) {
|
|
356
467
|
return { content: [{ type: 'text', text: 'Error: package_name is required' }] };
|
|
357
468
|
}
|
|
358
|
-
|
|
469
|
+
|
|
359
470
|
try {
|
|
360
471
|
const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(package_name)}`, {
|
|
361
472
|
signal: AbortSignal.timeout(10_000),
|
|
362
473
|
});
|
|
363
|
-
|
|
474
|
+
|
|
364
475
|
if (res.status === 404) {
|
|
365
|
-
return { content: [{ type: 'text', text: `Package "${package_name}" not found in registry.
|
|
476
|
+
return { content: [{ type: 'text', text: `Package "${package_name}" not found in registry.\n\nIt hasn't been audited yet. To audit it:\n1. Find the source URL (GitHub repo)\n2. Call audit_package with the URL\n3. Analyze the code\n4. Call submit_report with your findings` }] };
|
|
366
477
|
}
|
|
367
|
-
|
|
478
|
+
|
|
368
479
|
const data = await res.json();
|
|
369
|
-
|
|
480
|
+
const risk = data.risk_score ?? data.latest_risk_score ?? 'unknown';
|
|
481
|
+
const official = data.has_official_audit ? '✅ Officially audited' : 'Community audit';
|
|
482
|
+
|
|
483
|
+
let summary = `# ${package_name}\n\n`;
|
|
484
|
+
summary += `**Risk Score:** ${risk}/100\n`;
|
|
485
|
+
summary += `**Status:** ${official}\n`;
|
|
486
|
+
if (data.source_url) summary += `**Source:** ${data.source_url}\n`;
|
|
487
|
+
summary += `**Registry:** ${REGISTRY_URL}/skills/${package_name}\n\n`;
|
|
488
|
+
summary += `## Full Data\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``;
|
|
489
|
+
|
|
490
|
+
return { content: [{ type: 'text', text: summary }] };
|
|
370
491
|
} catch (err) {
|
|
371
492
|
return { content: [{ type: 'text', text: `Registry lookup failed: ${err.message}` }] };
|
|
372
493
|
}
|
|
373
494
|
}
|
|
374
|
-
|
|
495
|
+
|
|
375
496
|
default:
|
|
376
|
-
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
497
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}. Available: discover_servers, audit_package, submit_report, check_package` }] };
|
|
377
498
|
}
|
|
378
499
|
});
|
|
379
500
|
|