mpx-scan 1.0.2 → 1.1.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 +152 -94
- package/bin/cli.js +241 -58
- package/package.json +7 -2
- package/src/mcp.js +260 -0
- package/src/schema.js +198 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mpx-scan 🔍
|
|
2
2
|
|
|
3
|
-
**Professional website security scanner for developers**
|
|
3
|
+
**Professional website security scanner for developers and AI agents**
|
|
4
4
|
|
|
5
5
|
Check your site's security headers, SSL/TLS configuration, DNS settings, and get actionable fix suggestions — all from your terminal.
|
|
6
6
|
|
|
@@ -13,10 +13,14 @@ Part of the [Mesaplex](https://mesaplex.com) developer toolchain.
|
|
|
13
13
|
|
|
14
14
|
- **Zero-config security scanning** — just point it at a URL
|
|
15
15
|
- **Beautiful terminal output** with color-coded results
|
|
16
|
+
- **Structured JSON output** — `--json` for CI/CD and AI agent consumption
|
|
17
|
+
- **MCP server** — integrates with any MCP-compatible AI agent (Claude, GPT, Cursor, etc.)
|
|
16
18
|
- **Actionable fix suggestions** — copy-paste config for nginx, Apache, Caddy, Cloudflare
|
|
19
|
+
- **Batch scanning** — pipe URLs from stdin
|
|
20
|
+
- **Self-documenting** — `--schema` returns machine-readable tool description
|
|
17
21
|
- **Fast** — scans complete in seconds
|
|
18
22
|
- **Zero native dependencies** — installs cleanly everywhere
|
|
19
|
-
- **CI/CD ready** —
|
|
23
|
+
- **CI/CD ready** — predictable exit codes and JSON output
|
|
20
24
|
|
|
21
25
|
### Security Checks
|
|
22
26
|
|
|
@@ -50,40 +54,20 @@ mpx-scan https://example.com
|
|
|
50
54
|
mpx-scan https://example.com
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
### Get Fix Suggestions
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
mpx-scan https://example.com --fix nginx
|
|
59
|
-
mpx-scan https://example.com --fix apache
|
|
60
|
-
mpx-scan https://example.com --fix caddy
|
|
61
|
-
mpx-scan https://example.com --fix cloudflare
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Generates copy-paste configuration snippets for your platform.
|
|
65
|
-
|
|
66
|
-
### Deep Scan (Pro)
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
mpx-scan https://example.com --full
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Runs all security checks including DNS, cookies, SRI, exposed files.
|
|
73
|
-
|
|
74
|
-
### JSON Output (Pro)
|
|
57
|
+
### JSON Output
|
|
75
58
|
|
|
76
59
|
```bash
|
|
77
60
|
mpx-scan https://example.com --json
|
|
78
61
|
```
|
|
79
62
|
|
|
80
|
-
|
|
63
|
+
Returns structured JSON to stdout (progress/status goes to stderr):
|
|
81
64
|
|
|
82
65
|
```json
|
|
83
66
|
{
|
|
84
67
|
"mpxScan": {
|
|
85
|
-
"version": "1.
|
|
86
|
-
"scannedAt": "2026-02-
|
|
68
|
+
"version": "1.1.0",
|
|
69
|
+
"scannedAt": "2026-02-16T22:00:00.000Z",
|
|
70
|
+
"scanDuration": 350
|
|
87
71
|
},
|
|
88
72
|
"target": {
|
|
89
73
|
"url": "https://example.com",
|
|
@@ -98,28 +82,123 @@ Perfect for CI/CD pipelines:
|
|
|
98
82
|
"summary": {
|
|
99
83
|
"passed": 12,
|
|
100
84
|
"warnings": 3,
|
|
101
|
-
"failed": 2
|
|
102
|
-
|
|
85
|
+
"failed": 2,
|
|
86
|
+
"info": 0
|
|
87
|
+
},
|
|
88
|
+
"sections": { ... },
|
|
89
|
+
"tier": "free"
|
|
103
90
|
}
|
|
104
91
|
```
|
|
105
92
|
|
|
93
|
+
### Get Fix Suggestions
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
mpx-scan https://example.com --fix nginx
|
|
97
|
+
mpx-scan https://example.com --fix apache
|
|
98
|
+
mpx-scan https://example.com --fix caddy
|
|
99
|
+
mpx-scan https://example.com --fix cloudflare
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Deep Scan (Pro)
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
mpx-scan https://example.com --full
|
|
106
|
+
```
|
|
107
|
+
|
|
106
108
|
### Brief Output
|
|
107
109
|
|
|
108
110
|
```bash
|
|
109
111
|
mpx-scan https://example.com --brief
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
### Batch Scanning
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
```bash
|
|
117
|
+
cat urls.txt | mpx-scan --batch --json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Reads one URL per line from stdin, outputs one JSON result per line (JSONL format). Lines starting with `#` are ignored.
|
|
121
|
+
|
|
122
|
+
### Tool Schema
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
mpx-scan --schema
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Returns a JSON schema describing all commands, flags, inputs, and outputs — designed for AI agent tool discovery.
|
|
129
|
+
|
|
130
|
+
## 🤖 AI Agent Usage
|
|
131
|
+
|
|
132
|
+
mpx-scan is designed to be used by AI agents as well as humans.
|
|
133
|
+
|
|
134
|
+
### MCP Integration
|
|
135
|
+
|
|
136
|
+
Add to your MCP client configuration (Claude Desktop, Cursor, Windsurf, etc.):
|
|
115
137
|
|
|
116
|
-
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"mpx-scan": {
|
|
142
|
+
"command": "npx",
|
|
143
|
+
"args": ["mpx-scan", "mcp"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The MCP server exposes these tools:
|
|
150
|
+
- **`scan`** — Scan a URL and return structured results
|
|
151
|
+
- **`generate_fixes`** — Scan and generate platform-specific fix config
|
|
152
|
+
- **`get_schema`** — Get full tool schema
|
|
153
|
+
|
|
154
|
+
### Programmatic Usage
|
|
117
155
|
|
|
118
156
|
```bash
|
|
119
|
-
|
|
157
|
+
# JSON output for parsing
|
|
158
|
+
mpx-scan https://example.com --json
|
|
159
|
+
|
|
160
|
+
# Batch processing
|
|
161
|
+
cat urls.txt | mpx-scan --batch --json
|
|
162
|
+
|
|
163
|
+
# Schema discovery
|
|
164
|
+
mpx-scan --schema
|
|
165
|
+
|
|
166
|
+
# Quiet mode (no banners, progress goes to stderr)
|
|
167
|
+
mpx-scan https://example.com --json --quiet
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Exit Codes
|
|
171
|
+
|
|
172
|
+
| Code | Meaning |
|
|
173
|
+
|------|---------|
|
|
174
|
+
| 0 | Scan complete, no security issues found |
|
|
175
|
+
| 1 | Scan complete, security issues found |
|
|
176
|
+
| 2 | Invalid arguments |
|
|
177
|
+
| 3 | Configuration error (license, rate limit) |
|
|
178
|
+
| 4 | Network/connectivity error |
|
|
179
|
+
|
|
180
|
+
### Error Responses (JSON mode)
|
|
181
|
+
|
|
182
|
+
When `--json` is used, errors return structured JSON:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"error": "Description of what went wrong",
|
|
187
|
+
"code": "ERR_NETWORK"
|
|
188
|
+
}
|
|
120
189
|
```
|
|
121
190
|
|
|
122
|
-
|
|
191
|
+
Error codes: `ERR_NETWORK`, `ERR_SCAN`, `ERR_RATE_LIMIT`, `ERR_PRO_REQUIRED`, `ERR_NO_INPUT`
|
|
192
|
+
|
|
193
|
+
### Automation Tips
|
|
194
|
+
|
|
195
|
+
- Use `--json` for machine-parseable output (stdout only, no ANSI)
|
|
196
|
+
- Use `--no-color` to strip ANSI codes from human-readable output
|
|
197
|
+
- Use `--quiet` to suppress banners and progress info
|
|
198
|
+
- Pipe `--batch --json` for JSONL (one result per line) processing
|
|
199
|
+
- Check exit codes for pass/fail decisions in CI/CD
|
|
200
|
+
|
|
201
|
+
## 🎯 Use Cases
|
|
123
202
|
|
|
124
203
|
### CI/CD Integration
|
|
125
204
|
|
|
@@ -131,14 +210,17 @@ jobs:
|
|
|
131
210
|
scan:
|
|
132
211
|
runs-on: ubuntu-latest
|
|
133
212
|
steps:
|
|
134
|
-
- run: npx mpx-scan https://mysite.com --json
|
|
213
|
+
- run: npx mpx-scan https://mysite.com --ci --min-score 70 --json
|
|
135
214
|
```
|
|
136
215
|
|
|
137
|
-
###
|
|
216
|
+
### Monitoring Script
|
|
138
217
|
|
|
139
218
|
```bash
|
|
219
|
+
#!/bin/bash
|
|
140
220
|
for site in site1.com site2.com site3.com; do
|
|
141
|
-
mpx-scan $site --json
|
|
221
|
+
result=$(npx mpx-scan "$site" --json 2>/dev/null)
|
|
222
|
+
grade=$(echo "$result" | jq -r '.score.grade')
|
|
223
|
+
echo "$site: $grade"
|
|
142
224
|
done
|
|
143
225
|
```
|
|
144
226
|
|
|
@@ -150,38 +232,27 @@ done
|
|
|
150
232
|
| **Security headers** | ✅ | ✅ |
|
|
151
233
|
| **SSL/TLS checks** | ✅ | ✅ |
|
|
152
234
|
| **Server info checks** | ✅ | ✅ |
|
|
235
|
+
| **JSON output** | ✅ | ✅ |
|
|
236
|
+
| **Batch scanning** | ✅ | ✅ |
|
|
237
|
+
| **MCP server** | ✅ | ✅ |
|
|
153
238
|
| **DNS security** | ❌ | ✅ |
|
|
154
239
|
| **Cookie security** | ❌ | ✅ |
|
|
155
240
|
| **SRI checks** | ❌ | ✅ |
|
|
156
241
|
| **Exposed files** | ❌ | ✅ |
|
|
157
242
|
| **Mixed content** | ❌ | ✅ |
|
|
158
|
-
| **
|
|
159
|
-
| **Batch scanning** | ❌ | ✅ |
|
|
160
|
-
| **CI/CD integration** | ❌ | ✅ |
|
|
243
|
+
| **Full scan (--full)** | ❌ | ✅ |
|
|
161
244
|
|
|
162
245
|
**Upgrade to Pro:** [https://mesaplex.com/mpx-scan](https://mesaplex.com/mpx-scan)
|
|
163
246
|
|
|
164
247
|
## 🔐 License Management
|
|
165
248
|
|
|
166
|
-
### Check License Status
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
mpx-scan license
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Activate Pro License
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
mpx-scan activate MPX-PRO-XXXXXXXXXXXXXXXX
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Deactivate
|
|
179
|
-
|
|
180
249
|
```bash
|
|
181
|
-
mpx-scan
|
|
250
|
+
mpx-scan license # Check status
|
|
251
|
+
mpx-scan activate MPX-PRO-XXXXXXXX # Activate Pro
|
|
252
|
+
mpx-scan deactivate # Return to free tier
|
|
182
253
|
```
|
|
183
254
|
|
|
184
|
-
## 🛠️ CLI
|
|
255
|
+
## 🛠️ CLI Reference
|
|
185
256
|
|
|
186
257
|
```
|
|
187
258
|
Usage: mpx-scan [url] [options]
|
|
@@ -190,48 +261,44 @@ Arguments:
|
|
|
190
261
|
url URL to scan
|
|
191
262
|
|
|
192
263
|
Options:
|
|
193
|
-
-V, --version
|
|
264
|
+
-V, --version Output version number
|
|
265
|
+
--json Output as structured JSON
|
|
194
266
|
--full Run all checks (Pro only)
|
|
195
|
-
--
|
|
196
|
-
--
|
|
267
|
+
--brief Brief one-line output
|
|
268
|
+
--quiet, -q Minimal output (no banners)
|
|
269
|
+
--no-color Disable ANSI color codes
|
|
270
|
+
--batch Read URLs from stdin (one per line)
|
|
271
|
+
--schema Output JSON schema for tool discovery
|
|
197
272
|
--fix <platform> Generate fix config (nginx, apache, caddy, cloudflare)
|
|
198
|
-
--timeout <seconds> Connection timeout (default:
|
|
199
|
-
|
|
273
|
+
--timeout <seconds> Connection timeout (default: 10)
|
|
274
|
+
--ci CI mode: exit 1 if below --min-score
|
|
275
|
+
--min-score <score> Minimum score for CI mode (default: 70)
|
|
276
|
+
-h, --help Display help
|
|
200
277
|
|
|
201
278
|
Commands:
|
|
202
|
-
license
|
|
203
|
-
activate <key> Activate
|
|
204
|
-
deactivate
|
|
279
|
+
license Show license status
|
|
280
|
+
activate <key> Activate Pro license
|
|
281
|
+
deactivate Return to free tier
|
|
282
|
+
mcp Start MCP stdio server
|
|
205
283
|
```
|
|
206
284
|
|
|
207
285
|
## 📦 Installation
|
|
208
286
|
|
|
209
|
-
### Global Install
|
|
210
|
-
|
|
211
287
|
```bash
|
|
288
|
+
# Global
|
|
212
289
|
npm install -g mpx-scan
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Project Dependency
|
|
216
290
|
|
|
217
|
-
|
|
291
|
+
# Project dependency
|
|
218
292
|
npm install --save-dev mpx-scan
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Add to `package.json`:
|
|
222
293
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"scripts": {
|
|
226
|
-
"security": "mpx-scan https://mysite.com"
|
|
227
|
-
}
|
|
228
|
-
}
|
|
294
|
+
# One-off with npx
|
|
295
|
+
npx mpx-scan https://example.com
|
|
229
296
|
```
|
|
230
297
|
|
|
231
298
|
### Requirements
|
|
232
299
|
|
|
233
300
|
- Node.js 18.0.0 or higher
|
|
234
|
-
- No
|
|
301
|
+
- No native dependencies
|
|
235
302
|
- Works on macOS, Linux, Windows
|
|
236
303
|
|
|
237
304
|
## 🧪 Testing
|
|
@@ -240,11 +307,9 @@ Add to `package.json`:
|
|
|
240
307
|
npm test
|
|
241
308
|
```
|
|
242
309
|
|
|
243
|
-
Runs the built-in test suite for core scanning logic.
|
|
244
|
-
|
|
245
310
|
## 🤝 Contributing
|
|
246
311
|
|
|
247
|
-
|
|
312
|
+
Security improvements and bug fixes are welcome!
|
|
248
313
|
|
|
249
314
|
## 📄 License
|
|
250
315
|
|
|
@@ -255,22 +320,15 @@ See [LICENSE](LICENSE) for full terms.
|
|
|
255
320
|
## 🔗 Links
|
|
256
321
|
|
|
257
322
|
- **Website:** [https://mesaplex.com/mpx-scan](https://mesaplex.com/mpx-scan)
|
|
258
|
-
- **
|
|
323
|
+
- **npm:** [https://www.npmjs.com/package/mpx-scan](https://www.npmjs.com/package/mpx-scan)
|
|
324
|
+
- **GitHub:** [https://github.com/mesaplexdev/mpx-scan](https://github.com/mesaplexdev/mpx-scan)
|
|
259
325
|
- **Support:** support@mesaplex.com
|
|
260
|
-
- **Twitter:** [@mesaplex](https://twitter.com/mesaplex)
|
|
261
|
-
|
|
262
|
-
## 🐛 Known Issues
|
|
263
|
-
|
|
264
|
-
None currently! Report issues via email: support@mesaplex.com
|
|
265
326
|
|
|
266
327
|
## 📚 Related Tools
|
|
267
328
|
|
|
268
|
-
Part of the Mesaplex developer toolchain:
|
|
269
|
-
|
|
270
329
|
- **mpx-scan** — Security scanner (you are here)
|
|
271
|
-
- **mpx-api** — API testing toolkit
|
|
272
|
-
- **mpx-
|
|
273
|
-
- **mpx-deploy** — Deployment automation *(coming soon)*
|
|
330
|
+
- **[mpx-api](https://www.npmjs.com/package/mpx-api)** — API testing toolkit
|
|
331
|
+
- **[mpx-db](https://www.npmjs.com/package/mpx-db)** — Database toolkit
|
|
274
332
|
|
|
275
333
|
---
|
|
276
334
|
|
package/bin/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ const { scan } = require('../src/index');
|
|
|
13
13
|
const { formatReport, formatBrief } = require('../src/reporters/terminal');
|
|
14
14
|
const { formatJSON } = require('../src/reporters/json');
|
|
15
15
|
const { generateFixes, PLATFORMS } = require('../src/generators/fixes');
|
|
16
|
+
const { getSchema } = require('../src/schema');
|
|
16
17
|
const {
|
|
17
18
|
getLicense,
|
|
18
19
|
activateLicense,
|
|
@@ -24,6 +25,18 @@ const {
|
|
|
24
25
|
|
|
25
26
|
const pkg = require('../package.json');
|
|
26
27
|
|
|
28
|
+
// Exit codes per AI-native spec
|
|
29
|
+
const EXIT = {
|
|
30
|
+
SUCCESS: 0, // Success, no issues found
|
|
31
|
+
ISSUES_FOUND: 1, // Success, issues found
|
|
32
|
+
BAD_ARGS: 2, // Invalid arguments
|
|
33
|
+
CONFIG_ERROR: 3, // Configuration error
|
|
34
|
+
NETWORK_ERROR: 4 // Network/connectivity error
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Auto-detect non-interactive mode
|
|
38
|
+
const isInteractive = process.stdout.isTTY && !process.env.CI;
|
|
39
|
+
|
|
27
40
|
const program = new Command();
|
|
28
41
|
|
|
29
42
|
program
|
|
@@ -32,105 +45,252 @@ program
|
|
|
32
45
|
.version(pkg.version)
|
|
33
46
|
.argument('[url]', 'URL to scan')
|
|
34
47
|
.option('--full', 'Run all checks (Pro only)')
|
|
35
|
-
.option('--json', 'Output as JSON')
|
|
48
|
+
.option('--json', 'Output as JSON (machine-readable)')
|
|
36
49
|
.option('--brief', 'Brief output (one-line summary)')
|
|
50
|
+
.option('--quiet, -q', 'Minimal output (results only, no banners)')
|
|
51
|
+
.option('--no-color', 'Disable colored output')
|
|
52
|
+
.option('--batch', 'Batch mode: read URLs from stdin (one per line)')
|
|
53
|
+
.option('--schema', 'Output JSON schema describing all commands and flags')
|
|
37
54
|
.option('--fix <platform>', `Generate fix config for platform (${PLATFORMS.join(', ')})`)
|
|
38
55
|
.option('--timeout <seconds>', 'Connection timeout', '10')
|
|
39
56
|
.option('--ci', 'CI/CD mode: exit 1 if score below threshold')
|
|
40
57
|
.option('--min-score <score>', 'Minimum score for CI mode (default: 70)', '70')
|
|
41
58
|
.action(async (url, options) => {
|
|
59
|
+
// Handle --schema flag
|
|
60
|
+
if (options.schema) {
|
|
61
|
+
console.log(JSON.stringify(getSchema(), null, 2));
|
|
62
|
+
process.exit(EXIT.SUCCESS);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle --batch mode (read URLs from stdin)
|
|
67
|
+
if (options.batch) {
|
|
68
|
+
await runBatchMode(options);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
// Show help if no URL provided
|
|
43
73
|
if (!url) {
|
|
44
74
|
program.help();
|
|
45
75
|
return;
|
|
46
76
|
}
|
|
47
77
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
const exitCode = await runSingleScan(url, options);
|
|
79
|
+
process.exit(exitCode);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
async function runSingleScan(url, options) {
|
|
83
|
+
const jsonMode = options.json;
|
|
84
|
+
const quietMode = options.quiet || options.Q;
|
|
85
|
+
|
|
86
|
+
// Disable chalk if --no-color or non-TTY
|
|
87
|
+
if (options.color === false || !process.stdout.isTTY) {
|
|
88
|
+
chalk.level = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Check license and rate limits
|
|
93
|
+
const license = getLicense();
|
|
94
|
+
const rateLimit = checkRateLimit();
|
|
95
|
+
|
|
96
|
+
// Handle rate limiting
|
|
97
|
+
if (!rateLimit.allowed) {
|
|
98
|
+
if (jsonMode) {
|
|
99
|
+
console.log(JSON.stringify({
|
|
100
|
+
error: 'Daily scan limit reached',
|
|
101
|
+
code: 'ERR_RATE_LIMIT',
|
|
102
|
+
resetsAt: new Date(rateLimit.resetsAt).toISOString(),
|
|
103
|
+
limit: FREE_DAILY_LIMIT,
|
|
104
|
+
upgrade: 'https://mesaplex.com/mpx-scan'
|
|
105
|
+
}, null, 2));
|
|
106
|
+
} else {
|
|
55
107
|
console.error(chalk.red.bold('\n❌ Daily scan limit reached'));
|
|
56
108
|
console.error(chalk.yellow(`Free tier: ${FREE_DAILY_LIMIT} scans/day`));
|
|
57
109
|
console.error(chalk.gray(`Resets: ${new Date(rateLimit.resetsAt).toLocaleString()}\n`));
|
|
58
110
|
console.error(chalk.blue('Upgrade to Pro for unlimited scans:'));
|
|
59
111
|
console.error(chalk.blue(' https://mesaplex.com/mpx-scan\n'));
|
|
60
|
-
process.exit(1);
|
|
61
112
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
113
|
+
return EXIT.CONFIG_ERROR;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for Pro-only features
|
|
117
|
+
if (options.full && license.tier !== 'pro') {
|
|
118
|
+
if (jsonMode) {
|
|
119
|
+
console.log(JSON.stringify({
|
|
120
|
+
error: '--full flag requires Pro license',
|
|
121
|
+
code: 'ERR_PRO_REQUIRED',
|
|
122
|
+
upgrade: 'https://mesaplex.com/mpx-scan'
|
|
123
|
+
}, null, 2));
|
|
124
|
+
} else {
|
|
65
125
|
console.error(chalk.red.bold('\n❌ --full flag requires Pro license'));
|
|
66
126
|
console.error(chalk.yellow('Free tier includes: headers, SSL, server checks'));
|
|
67
127
|
console.error(chalk.yellow('Pro includes: all checks (DNS, cookies, SRI, exposed files, etc.)\n'));
|
|
68
128
|
console.error(chalk.blue('Upgrade: https://mesaplex.com/mpx-scan\n'));
|
|
69
|
-
process.exit(1);
|
|
70
129
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
130
|
+
return EXIT.CONFIG_ERROR;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Show scan info (unless quiet/json/brief)
|
|
134
|
+
if (!jsonMode && !options.brief && !quietMode) {
|
|
135
|
+
console.error(chalk.bold.cyan('🔍 Scanning...'));
|
|
136
|
+
if (license.tier === 'free') {
|
|
137
|
+
console.error(chalk.gray(`Free tier: ${rateLimit.remaining} scan(s) remaining today`));
|
|
76
138
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Run scan
|
|
142
|
+
const results = await scan(url, {
|
|
143
|
+
timeout: parseInt(options.timeout) * 1000,
|
|
144
|
+
tier: license.tier,
|
|
145
|
+
full: options.full
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Record scan for rate limiting
|
|
149
|
+
recordScan();
|
|
150
|
+
|
|
151
|
+
// Output results
|
|
152
|
+
if (options.fix) {
|
|
153
|
+
console.log(generateFixes(options.fix, results));
|
|
154
|
+
} else if (jsonMode) {
|
|
155
|
+
console.log(formatJSON(results, true));
|
|
156
|
+
} else if (options.brief) {
|
|
157
|
+
console.log(formatBrief(results));
|
|
158
|
+
} else {
|
|
159
|
+
console.log(formatReport(results, { ...options, quiet: quietMode }));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Determine exit code based on findings
|
|
163
|
+
if (options.ci) {
|
|
164
|
+
const minScore = parseInt(options.minScore);
|
|
165
|
+
const percentage = Math.round((results.score / results.maxScore) * 100);
|
|
166
|
+
if (percentage < minScore) {
|
|
167
|
+
if (!jsonMode && !options.brief && !quietMode) {
|
|
168
|
+
console.error(chalk.yellow(`\n⚠️ CI mode: Score ${percentage}/100 below minimum ${minScore}`));
|
|
84
169
|
}
|
|
170
|
+
return EXIT.ISSUES_FOUND;
|
|
85
171
|
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Exit 1 if there are failures, 0 if clean
|
|
175
|
+
if (results.summary.failed > 0) {
|
|
176
|
+
return EXIT.ISSUES_FOUND;
|
|
177
|
+
}
|
|
178
|
+
return EXIT.SUCCESS;
|
|
179
|
+
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (jsonMode) {
|
|
182
|
+
const code = isNetworkError(err) ? 'ERR_NETWORK' : 'ERR_SCAN';
|
|
183
|
+
console.log(JSON.stringify({ error: err.message, code }, null, 2));
|
|
184
|
+
} else {
|
|
185
|
+
console.error(chalk.red.bold('\n❌ Error:'), err.message);
|
|
186
|
+
console.error('');
|
|
187
|
+
}
|
|
188
|
+
return isNetworkError(err) ? EXIT.NETWORK_ERROR : EXIT.ISSUES_FOUND;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function runBatchMode(options) {
|
|
193
|
+
const jsonMode = options.json;
|
|
194
|
+
|
|
195
|
+
// Read URLs from stdin
|
|
196
|
+
const input = await readStdin();
|
|
197
|
+
if (!input.trim()) {
|
|
198
|
+
if (jsonMode) {
|
|
199
|
+
console.log(JSON.stringify({ error: 'No URLs provided on stdin', code: 'ERR_NO_INPUT' }, null, 2));
|
|
200
|
+
} else {
|
|
201
|
+
console.error(chalk.red('No URLs provided. Pipe URLs via stdin:'));
|
|
202
|
+
console.error(chalk.gray(' cat urls.txt | mpx-scan --batch --json'));
|
|
203
|
+
}
|
|
204
|
+
process.exit(EXIT.BAD_ARGS);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const urls = input.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
209
|
+
|
|
210
|
+
if (urls.length === 0) {
|
|
211
|
+
if (jsonMode) {
|
|
212
|
+
console.log(JSON.stringify({ error: 'No valid URLs found in input', code: 'ERR_NO_INPUT' }, null, 2));
|
|
213
|
+
} else {
|
|
214
|
+
console.error(chalk.red('No valid URLs found in input.'));
|
|
215
|
+
}
|
|
216
|
+
process.exit(EXIT.BAD_ARGS);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let hasIssues = false;
|
|
221
|
+
let hasErrors = false;
|
|
222
|
+
|
|
223
|
+
for (const url of urls) {
|
|
224
|
+
try {
|
|
225
|
+
const license = getLicense();
|
|
226
|
+
const rateLimit = checkRateLimit();
|
|
86
227
|
|
|
87
|
-
|
|
228
|
+
if (!rateLimit.allowed) {
|
|
229
|
+
if (jsonMode) {
|
|
230
|
+
console.log(JSON.stringify({
|
|
231
|
+
url,
|
|
232
|
+
error: 'Rate limit reached',
|
|
233
|
+
code: 'ERR_RATE_LIMIT'
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
hasErrors = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
88
240
|
const results = await scan(url, {
|
|
89
241
|
timeout: parseInt(options.timeout) * 1000,
|
|
90
242
|
tier: license.tier,
|
|
91
243
|
full: options.full
|
|
92
244
|
});
|
|
93
245
|
|
|
94
|
-
// Record scan for rate limiting
|
|
95
246
|
recordScan();
|
|
96
247
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.log(formatJSON(results,
|
|
248
|
+
if (results.summary.failed > 0) hasIssues = true;
|
|
249
|
+
|
|
250
|
+
if (jsonMode) {
|
|
251
|
+
// JSONL: one JSON object per line
|
|
252
|
+
console.log(formatJSON(results, false));
|
|
102
253
|
} else if (options.brief) {
|
|
103
254
|
console.log(formatBrief(results));
|
|
104
255
|
} else {
|
|
105
256
|
console.log(formatReport(results, options));
|
|
106
257
|
}
|
|
107
|
-
|
|
108
|
-
// Exit code logic:
|
|
109
|
-
// - Exit 0: scan completed successfully (default)
|
|
110
|
-
// - Exit 1: only in --ci mode if score below threshold
|
|
111
|
-
if (options.ci) {
|
|
112
|
-
const minScore = parseInt(options.minScore);
|
|
113
|
-
const percentage = Math.round((results.score / results.maxScore) * 100);
|
|
114
|
-
if (percentage < minScore) {
|
|
115
|
-
if (!options.json && !options.brief) {
|
|
116
|
-
console.error(chalk.yellow(`\n⚠️ CI mode: Score ${percentage}/100 below minimum ${minScore}`));
|
|
117
|
-
}
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
process.exit(0);
|
|
123
|
-
|
|
124
258
|
} catch (err) {
|
|
125
|
-
|
|
126
|
-
|
|
259
|
+
hasErrors = true;
|
|
260
|
+
if (jsonMode) {
|
|
261
|
+
console.log(JSON.stringify({ url, error: err.message, code: isNetworkError(err) ? 'ERR_NETWORK' : 'ERR_SCAN' }));
|
|
127
262
|
} else {
|
|
128
|
-
console.error(chalk.red
|
|
129
|
-
console.error('');
|
|
263
|
+
console.error(chalk.red(`Error scanning ${url}: ${err.message}`));
|
|
130
264
|
}
|
|
131
|
-
process.exit(1);
|
|
132
265
|
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (hasErrors) process.exit(EXIT.NETWORK_ERROR);
|
|
269
|
+
if (hasIssues) process.exit(EXIT.ISSUES_FOUND);
|
|
270
|
+
process.exit(EXIT.SUCCESS);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readStdin() {
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
if (process.stdin.isTTY) {
|
|
276
|
+
resolve('');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
let data = '';
|
|
280
|
+
process.stdin.setEncoding('utf8');
|
|
281
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
282
|
+
process.stdin.on('end', () => resolve(data));
|
|
133
283
|
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isNetworkError(err) {
|
|
287
|
+
const msg = (err.message || '').toLowerCase();
|
|
288
|
+
return msg.includes('econnrefused') || msg.includes('enotfound') ||
|
|
289
|
+
msg.includes('timeout') || msg.includes('network') ||
|
|
290
|
+
msg.includes('dns') || msg.includes('econnreset') ||
|
|
291
|
+
err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' ||
|
|
292
|
+
err.code === 'ETIMEDOUT';
|
|
293
|
+
}
|
|
134
294
|
|
|
135
295
|
// License management subcommands
|
|
136
296
|
program
|
|
@@ -187,7 +347,7 @@ program
|
|
|
187
347
|
} catch (err) {
|
|
188
348
|
console.error(chalk.red.bold('\n❌ Activation failed:'), err.message);
|
|
189
349
|
console.error('');
|
|
190
|
-
process.exit(
|
|
350
|
+
process.exit(EXIT.CONFIG_ERROR);
|
|
191
351
|
}
|
|
192
352
|
});
|
|
193
353
|
|
|
@@ -202,20 +362,43 @@ program
|
|
|
202
362
|
console.log('');
|
|
203
363
|
});
|
|
204
364
|
|
|
365
|
+
// MCP subcommand
|
|
366
|
+
program
|
|
367
|
+
.command('mcp')
|
|
368
|
+
.description('Start MCP (Model Context Protocol) stdio server')
|
|
369
|
+
.action(async () => {
|
|
370
|
+
try {
|
|
371
|
+
const { startMCPServer } = require('../src/mcp');
|
|
372
|
+
await startMCPServer();
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.error(JSON.stringify({ error: err.message, code: 'ERR_MCP_START' }));
|
|
375
|
+
process.exit(EXIT.CONFIG_ERROR);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
205
379
|
// Examples
|
|
206
380
|
program.addHelpText('after', `
|
|
207
381
|
${chalk.bold('Examples:')}
|
|
208
382
|
${chalk.cyan('mpx-scan https://example.com')} Quick security scan
|
|
209
383
|
${chalk.cyan('mpx-scan example.com --full')} Deep scan (Pro only)
|
|
210
|
-
${chalk.cyan('mpx-scan example.com --json')} JSON output
|
|
384
|
+
${chalk.cyan('mpx-scan example.com --json')} JSON output
|
|
211
385
|
${chalk.cyan('mpx-scan example.com --fix nginx')} Generate nginx config
|
|
212
386
|
${chalk.cyan('mpx-scan example.com --brief')} One-line summary
|
|
387
|
+
${chalk.cyan('mpx-scan --schema')} Show tool schema (JSON)
|
|
388
|
+
${chalk.cyan('cat urls.txt | mpx-scan --batch --json')} Batch scan from stdin
|
|
389
|
+
${chalk.cyan('mpx-scan mcp')} Start MCP server
|
|
213
390
|
${chalk.cyan('mpx-scan license')} Check license status
|
|
214
|
-
|
|
391
|
+
|
|
392
|
+
${chalk.bold('Exit Codes:')}
|
|
393
|
+
0 Success, no issues found
|
|
394
|
+
1 Success, issues found
|
|
395
|
+
2 Invalid arguments
|
|
396
|
+
3 Configuration error (license, rate limit)
|
|
397
|
+
4 Network/connectivity error
|
|
215
398
|
|
|
216
399
|
${chalk.bold('Free vs Pro:')}
|
|
217
400
|
${chalk.yellow('Free:')} 3 scans/day, basic checks (headers, SSL, server)
|
|
218
|
-
${chalk.green('Pro:')} Unlimited scans, all checks,
|
|
401
|
+
${chalk.green('Pro:')} Unlimited scans, all checks, batch mode, CI/CD integration
|
|
219
402
|
|
|
220
403
|
${chalk.blue('Upgrade: https://mesaplex.com/mpx-scan')}
|
|
221
404
|
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mpx-scan",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Professional website security scanner CLI. Check headers, SSL, cookies, DNS, and get actionable fix suggestions. Part of the Mesaplex developer toolchain.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
"security-headers",
|
|
26
26
|
"ssl-check",
|
|
27
27
|
"dns-security",
|
|
28
|
-
"cors"
|
|
28
|
+
"cors",
|
|
29
|
+
"mcp",
|
|
30
|
+
"ai-native",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"automation"
|
|
29
33
|
],
|
|
30
34
|
"author": "Mesaplex <support@mesaplex.com>",
|
|
31
35
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
"node": ">=18.0.0"
|
|
40
44
|
},
|
|
41
45
|
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
42
47
|
"chalk": "^4.1.2",
|
|
43
48
|
"commander": "^12.0.0"
|
|
44
49
|
},
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) Server
|
|
3
|
+
*
|
|
4
|
+
* Exposes mpx-scan capabilities as MCP tools for AI agent integration.
|
|
5
|
+
* Runs over stdio transport.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
9
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
10
|
+
const {
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
CallToolRequestSchema
|
|
13
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
14
|
+
|
|
15
|
+
const { scan } = require('./index');
|
|
16
|
+
const { formatJSON } = require('./reporters/json');
|
|
17
|
+
const { getSchema } = require('./schema');
|
|
18
|
+
const { getLicense, checkRateLimit, recordScan } = require('./license');
|
|
19
|
+
const { generateFixes, PLATFORMS } = require('./generators/fixes');
|
|
20
|
+
const pkg = require('../package.json');
|
|
21
|
+
|
|
22
|
+
async function startMCPServer() {
|
|
23
|
+
const server = new Server(
|
|
24
|
+
{ name: 'mpx-scan', version: pkg.version },
|
|
25
|
+
{ capabilities: { tools: {} } }
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// List available tools
|
|
29
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
30
|
+
return {
|
|
31
|
+
tools: [
|
|
32
|
+
{
|
|
33
|
+
name: 'scan',
|
|
34
|
+
description: 'Scan a website for security issues. Returns structured results with grade, score, and per-check details.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
url: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'URL to scan (https:// added automatically if missing)'
|
|
41
|
+
},
|
|
42
|
+
full: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'Run all security checks (Pro license required)',
|
|
45
|
+
default: false
|
|
46
|
+
},
|
|
47
|
+
timeout: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
description: 'Connection timeout in seconds',
|
|
50
|
+
default: 10
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
required: ['url']
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'generate_fixes',
|
|
58
|
+
description: 'Generate platform-specific configuration to fix security issues found by a scan.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
url: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'URL to scan and generate fixes for'
|
|
65
|
+
},
|
|
66
|
+
platform: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
enum: PLATFORMS,
|
|
69
|
+
description: 'Target platform for fix configuration'
|
|
70
|
+
},
|
|
71
|
+
timeout: {
|
|
72
|
+
type: 'number',
|
|
73
|
+
description: 'Connection timeout in seconds',
|
|
74
|
+
default: 10
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ['url', 'platform']
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'get_schema',
|
|
82
|
+
description: 'Get the full JSON schema describing all mpx-scan commands, flags, and output formats.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Handle tool calls
|
|
93
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
94
|
+
const { name, arguments: args } = request.params;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
switch (name) {
|
|
98
|
+
case 'scan': {
|
|
99
|
+
const license = getLicense();
|
|
100
|
+
const rateLimit = checkRateLimit();
|
|
101
|
+
|
|
102
|
+
if (!rateLimit.allowed) {
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: JSON.stringify({
|
|
107
|
+
error: 'Daily scan limit reached',
|
|
108
|
+
code: 'ERR_RATE_LIMIT',
|
|
109
|
+
resetsAt: new Date(rateLimit.resetsAt).toISOString()
|
|
110
|
+
}, null, 2)
|
|
111
|
+
}],
|
|
112
|
+
isError: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const results = await scan(args.url, {
|
|
117
|
+
timeout: (args.timeout || 10) * 1000,
|
|
118
|
+
tier: license.tier,
|
|
119
|
+
full: args.full || false
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
recordScan();
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: 'text',
|
|
127
|
+
text: formatJSON(results, true)
|
|
128
|
+
}]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'generate_fixes': {
|
|
133
|
+
const license = getLicense();
|
|
134
|
+
const rateLimit2 = checkRateLimit();
|
|
135
|
+
|
|
136
|
+
if (!rateLimit2.allowed) {
|
|
137
|
+
return {
|
|
138
|
+
content: [{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: JSON.stringify({
|
|
141
|
+
error: 'Daily scan limit reached',
|
|
142
|
+
code: 'ERR_RATE_LIMIT',
|
|
143
|
+
resetsAt: new Date(rateLimit2.resetsAt).toISOString()
|
|
144
|
+
}, null, 2)
|
|
145
|
+
}],
|
|
146
|
+
isError: true
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fixResults = await scan(args.url, {
|
|
151
|
+
timeout: (args.timeout || 10) * 1000,
|
|
152
|
+
tier: license.tier,
|
|
153
|
+
full: false
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
recordScan();
|
|
157
|
+
|
|
158
|
+
// Collect issues and generate structured fix data
|
|
159
|
+
const issues = [];
|
|
160
|
+
for (const [sectionName, section] of Object.entries(fixResults.sections)) {
|
|
161
|
+
for (const check of section.checks) {
|
|
162
|
+
if ((check.status === 'fail' || check.status === 'warn') && check.recommendation) {
|
|
163
|
+
issues.push({
|
|
164
|
+
section: sectionName,
|
|
165
|
+
name: check.name,
|
|
166
|
+
status: check.status,
|
|
167
|
+
recommendation: check.recommendation
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Extract headers from issues
|
|
174
|
+
const headers = {};
|
|
175
|
+
for (const issue of issues) {
|
|
176
|
+
const rec = issue.recommendation;
|
|
177
|
+
if (rec.includes('Strict-Transport-Security')) headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload';
|
|
178
|
+
if (rec.includes('Content-Security-Policy')) headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'";
|
|
179
|
+
if (rec.includes('X-Content-Type-Options')) headers['X-Content-Type-Options'] = 'nosniff';
|
|
180
|
+
if (rec.includes('X-Frame-Options')) headers['X-Frame-Options'] = 'DENY';
|
|
181
|
+
if (rec.includes('Referrer-Policy')) headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
|
|
182
|
+
if (rec.includes('Permissions-Policy')) headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()';
|
|
183
|
+
if (rec.includes('Cross-Origin-Opener-Policy')) headers['Cross-Origin-Opener-Policy'] = 'same-origin';
|
|
184
|
+
if (rec.includes('Cross-Origin-Resource-Policy')) headers['Cross-Origin-Resource-Policy'] = 'same-origin';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const hasSSL = issues.some(i => i.section === 'ssl');
|
|
188
|
+
|
|
189
|
+
// Build platform-specific config snippet
|
|
190
|
+
let configSnippet = '';
|
|
191
|
+
const p = args.platform;
|
|
192
|
+
if (p === 'nginx') {
|
|
193
|
+
const headerLines = Object.entries(headers).map(([h, v]) => ` add_header ${h} "${v}" always;`).join('\n');
|
|
194
|
+
configSnippet = `server {\n # ... your existing config ...\n\n${headerLines}\n}`;
|
|
195
|
+
if (hasSSL) configSnippet += '\n\n# SSL/TLS\nssl_protocols TLSv1.2 TLSv1.3;\nssl_prefer_server_ciphers on;';
|
|
196
|
+
} else if (p === 'apache') {
|
|
197
|
+
const headerLines = Object.entries(headers).map(([h, v]) => ` Header always set ${h} "${v}"`).join('\n');
|
|
198
|
+
configSnippet = `<IfModule mod_headers.c>\n${headerLines}\n</IfModule>`;
|
|
199
|
+
} else if (p === 'caddy') {
|
|
200
|
+
const headerLines = Object.entries(headers).map(([h, v]) => ` ${h} "${v}"`).join('\n');
|
|
201
|
+
configSnippet = `header {\n${headerLines}\n}`;
|
|
202
|
+
} else if (p === 'cloudflare') {
|
|
203
|
+
configSnippet = Object.entries(headers).map(([h, v]) => `Set "${h}" to "${v}"`).join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fixData = {
|
|
207
|
+
url: args.url,
|
|
208
|
+
platform: args.platform,
|
|
209
|
+
issueCount: issues.length,
|
|
210
|
+
issues: issues.map(i => ({ section: i.section, name: i.name, status: i.status, recommendation: i.recommendation })),
|
|
211
|
+
headers,
|
|
212
|
+
hasSSLIssues: hasSSL,
|
|
213
|
+
configSnippet,
|
|
214
|
+
instructions: {
|
|
215
|
+
nginx: 'Add to server {} block, then: sudo nginx -t && sudo systemctl reload nginx',
|
|
216
|
+
apache: 'Add to .htaccess or site config, then: sudo apachectl configtest && sudo systemctl reload apache2',
|
|
217
|
+
caddy: 'Add to Caddyfile site block, then: sudo systemctl reload caddy',
|
|
218
|
+
cloudflare: 'Dashboard → Rules → Transform Rules → Modify Response Header'
|
|
219
|
+
}[args.platform]
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: 'text',
|
|
225
|
+
text: JSON.stringify(fixData, null, 2)
|
|
226
|
+
}]
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'get_schema': {
|
|
231
|
+
return {
|
|
232
|
+
content: [{
|
|
233
|
+
type: 'text',
|
|
234
|
+
text: JSON.stringify(getSchema(), null, 2)
|
|
235
|
+
}]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
default:
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
242
|
+
isError: true
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
return {
|
|
247
|
+
content: [{
|
|
248
|
+
type: 'text',
|
|
249
|
+
text: JSON.stringify({ error: err.message, code: 'ERR_SCAN' }, null, 2)
|
|
250
|
+
}],
|
|
251
|
+
isError: true
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const transport = new StdioServerTransport();
|
|
257
|
+
await server.connect(transport);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = { startMCPServer };
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Module
|
|
3
|
+
*
|
|
4
|
+
* Returns a machine-readable JSON schema describing all commands,
|
|
5
|
+
* flags, inputs, and outputs for AI agent discovery.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const pkg = require('../package.json');
|
|
9
|
+
const { PLATFORMS } = require('./generators/fixes');
|
|
10
|
+
const { SCANNER_TIERS } = require('./index');
|
|
11
|
+
|
|
12
|
+
function getSchema() {
|
|
13
|
+
return {
|
|
14
|
+
tool: 'mpx-scan',
|
|
15
|
+
version: pkg.version,
|
|
16
|
+
description: pkg.description,
|
|
17
|
+
homepage: pkg.homepage,
|
|
18
|
+
commands: {
|
|
19
|
+
scan: {
|
|
20
|
+
description: 'Scan a URL for security issues',
|
|
21
|
+
usage: 'mpx-scan <url> [options]',
|
|
22
|
+
arguments: {
|
|
23
|
+
url: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
required: true,
|
|
26
|
+
description: 'URL to scan (https:// prefix added automatically if missing)'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
flags: {
|
|
30
|
+
'--json': {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
default: false,
|
|
33
|
+
description: 'Output results as structured JSON'
|
|
34
|
+
},
|
|
35
|
+
'--full': {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
default: false,
|
|
38
|
+
description: 'Run all security checks (Pro license required)'
|
|
39
|
+
},
|
|
40
|
+
'--brief': {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
default: false,
|
|
43
|
+
description: 'One-line summary output'
|
|
44
|
+
},
|
|
45
|
+
'--quiet': {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
default: false,
|
|
48
|
+
description: 'Minimal output (no banners or progress)'
|
|
49
|
+
},
|
|
50
|
+
'--no-color': {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
default: false,
|
|
53
|
+
description: 'Disable ANSI color codes in output'
|
|
54
|
+
},
|
|
55
|
+
'--batch': {
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
default: false,
|
|
58
|
+
description: 'Read URLs from stdin (one per line), output JSONL with --json'
|
|
59
|
+
},
|
|
60
|
+
'--fix': {
|
|
61
|
+
type: 'string',
|
|
62
|
+
enum: PLATFORMS,
|
|
63
|
+
description: 'Generate fix configuration for specified platform'
|
|
64
|
+
},
|
|
65
|
+
'--timeout': {
|
|
66
|
+
type: 'number',
|
|
67
|
+
default: 10,
|
|
68
|
+
description: 'Connection timeout in seconds'
|
|
69
|
+
},
|
|
70
|
+
'--ci': {
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
default: false,
|
|
73
|
+
description: 'CI/CD mode: exit 1 if score below --min-score'
|
|
74
|
+
},
|
|
75
|
+
'--min-score': {
|
|
76
|
+
type: 'number',
|
|
77
|
+
default: 70,
|
|
78
|
+
description: 'Minimum score threshold for --ci mode (0-100)'
|
|
79
|
+
},
|
|
80
|
+
'--schema': {
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
default: false,
|
|
83
|
+
description: 'Output this schema as JSON'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
output: {
|
|
87
|
+
json: {
|
|
88
|
+
description: 'Structured scan results when --json is used',
|
|
89
|
+
schema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
mpxScan: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
version: { type: 'string' },
|
|
96
|
+
scannedAt: { type: 'string', format: 'date-time' },
|
|
97
|
+
scanDuration: { type: 'number', description: 'Duration in milliseconds' }
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
target: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
url: { type: 'string' },
|
|
104
|
+
hostname: { type: 'string' }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
score: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
grade: { type: 'string', enum: ['A+', 'A', 'B', 'C', 'D', 'F'] },
|
|
111
|
+
numeric: { type: 'number' },
|
|
112
|
+
maxScore: { type: 'number' },
|
|
113
|
+
percentage: { type: 'number', minimum: 0, maximum: 100 }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
summary: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
passed: { type: 'number' },
|
|
120
|
+
warnings: { type: 'number' },
|
|
121
|
+
failed: { type: 'number' },
|
|
122
|
+
info: { type: 'number' }
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
sections: { type: 'object', description: 'Per-scanner results keyed by scanner name' },
|
|
126
|
+
tier: { type: 'string', enum: ['free', 'pro'] }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
error: {
|
|
131
|
+
description: 'Error response when scan fails',
|
|
132
|
+
schema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
error: { type: 'string' },
|
|
136
|
+
code: { type: 'string', enum: ['ERR_NETWORK', 'ERR_SCAN', 'ERR_RATE_LIMIT', 'ERR_PRO_REQUIRED', 'ERR_NO_INPUT'] }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
exitCodes: {
|
|
142
|
+
0: 'Success, no security issues found',
|
|
143
|
+
1: 'Success, security issues found',
|
|
144
|
+
2: 'Invalid arguments',
|
|
145
|
+
3: 'Configuration error (license, rate limit)',
|
|
146
|
+
4: 'Network/connectivity error'
|
|
147
|
+
},
|
|
148
|
+
examples: [
|
|
149
|
+
{ command: 'mpx-scan https://example.com --json', description: 'Scan with JSON output' },
|
|
150
|
+
{ command: 'mpx-scan https://example.com --json --full', description: 'Full scan with JSON (Pro)' },
|
|
151
|
+
{ command: 'cat urls.txt | mpx-scan --batch --json', description: 'Batch scan from stdin' },
|
|
152
|
+
{ command: 'mpx-scan https://example.com --fix nginx', description: 'Get nginx fix config' }
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
mcp: {
|
|
156
|
+
description: 'Start MCP (Model Context Protocol) stdio server for AI agent integration',
|
|
157
|
+
usage: 'mpx-scan mcp',
|
|
158
|
+
arguments: {},
|
|
159
|
+
flags: {},
|
|
160
|
+
examples: [
|
|
161
|
+
{ command: 'mpx-scan mcp', description: 'Start MCP stdio server' }
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
license: {
|
|
165
|
+
description: 'Show current license status',
|
|
166
|
+
usage: 'mpx-scan license'
|
|
167
|
+
},
|
|
168
|
+
activate: {
|
|
169
|
+
description: 'Activate a Pro license key',
|
|
170
|
+
usage: 'mpx-scan activate <key>',
|
|
171
|
+
arguments: {
|
|
172
|
+
key: { type: 'string', required: true, description: 'License key (MPX-PRO-...)' }
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
deactivate: {
|
|
176
|
+
description: 'Deactivate Pro license and return to free tier',
|
|
177
|
+
usage: 'mpx-scan deactivate'
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
scanners: {
|
|
181
|
+
free: SCANNER_TIERS.free,
|
|
182
|
+
pro: SCANNER_TIERS.pro
|
|
183
|
+
},
|
|
184
|
+
mcpConfig: {
|
|
185
|
+
description: 'Add to your MCP client configuration to use mpx-scan as an AI tool',
|
|
186
|
+
config: {
|
|
187
|
+
mcpServers: {
|
|
188
|
+
'mpx-scan': {
|
|
189
|
+
command: 'npx',
|
|
190
|
+
args: ['mpx-scan', 'mcp']
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { getSchema };
|