mpx-scan 1.3.0 โ 1.3.1
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 +110 -147
- package/bin/cli.js +60 -21
- package/package.json +19 -15
- package/src/index.js +2 -2
- package/src/reporters/pdf.js +218 -0
- package/src/scanners/cookies.js +1 -1
- package/src/scanners/dns.js +8 -2
- package/src/scanners/exposed-files.js +1 -1
- package/src/scanners/fingerprint.js +1 -1
- package/src/scanners/headers.js +1 -1
- package/src/scanners/redirects.js +1 -1
- package/src/scanners/server.js +3 -3
- package/src/scanners/sri.js +1 -1
- package/src/schema.js +34 -0
package/README.md
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
# mpx-scan ๐
|
|
2
2
|
|
|
3
|
-
**Professional website security scanner for developers and AI agents
|
|
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
|
|
|
7
7
|
Part of the [Mesaplex](https://mesaplex.com) developer toolchain.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/mpx-scan)
|
|
10
|
-
[](LICENSE)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## Features
|
|
13
14
|
|
|
14
15
|
- **Zero-config security scanning** โ just point it at a URL
|
|
15
16
|
- **Beautiful terminal output** with color-coded results
|
|
16
17
|
- **Structured JSON output** โ `--json` for CI/CD and AI agent consumption
|
|
17
|
-
- **MCP server** โ integrates with any MCP-compatible AI agent (Claude,
|
|
18
|
+
- **MCP server** โ integrates with any MCP-compatible AI agent (Claude, Cursor, Windsurf, etc.)
|
|
18
19
|
- **Actionable fix suggestions** โ copy-paste config for nginx, Apache, Caddy, Cloudflare
|
|
19
20
|
- **Batch scanning** โ pipe URLs from stdin
|
|
20
21
|
- **Self-documenting** โ `--schema` returns machine-readable tool description
|
|
21
|
-
- **Fast** โ scans complete in seconds
|
|
22
22
|
- **Zero native dependencies** โ installs cleanly everywhere
|
|
23
|
-
- **CI/CD ready** โ predictable exit codes and JSON output
|
|
24
23
|
|
|
25
24
|
### Security Checks
|
|
26
25
|
|
|
@@ -30,23 +29,42 @@ Part of the [Mesaplex](https://mesaplex.com) developer toolchain.
|
|
|
30
29
|
- โ
Server information leakage
|
|
31
30
|
- โ
CORS misconfiguration
|
|
32
31
|
- โ
Mixed content detection
|
|
33
|
-
- โ
DNS security (DNSSEC, CAA records) โ *Pro
|
|
34
|
-
- โ
Subresource Integrity (SRI) โ *Pro
|
|
35
|
-
- โ
Open redirect detection โ *Pro
|
|
36
|
-
- โ
Exposed sensitive files โ *Pro
|
|
32
|
+
- โ
DNS security (DNSSEC, CAA records) โ *Pro*
|
|
33
|
+
- โ
Subresource Integrity (SRI) โ *Pro*
|
|
34
|
+
- โ
Open redirect detection โ *Pro*
|
|
35
|
+
- โ
Exposed sensitive files โ *Pro*
|
|
37
36
|
|
|
38
|
-
##
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g mpx-scan
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or run directly with npx:
|
|
39
44
|
|
|
40
45
|
```bash
|
|
41
|
-
# Run once without installing
|
|
42
46
|
npx mpx-scan https://example.com
|
|
47
|
+
```
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
**Requirements:** Node.js 18+ ยท No native dependencies ยท macOS, Linux, Windows
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Basic scan
|
|
46
55
|
mpx-scan https://example.com
|
|
56
|
+
|
|
57
|
+
# JSON output
|
|
58
|
+
mpx-scan https://example.com --json
|
|
59
|
+
|
|
60
|
+
# Fix suggestions for nginx
|
|
61
|
+
mpx-scan https://example.com --fix nginx
|
|
62
|
+
|
|
63
|
+
# Deep scan (Pro)
|
|
64
|
+
mpx-scan https://example.com --full
|
|
47
65
|
```
|
|
48
66
|
|
|
49
|
-
##
|
|
67
|
+
## Usage
|
|
50
68
|
|
|
51
69
|
### Basic Scan
|
|
52
70
|
|
|
@@ -65,7 +83,7 @@ Returns structured JSON to stdout (progress/status goes to stderr):
|
|
|
65
83
|
```json
|
|
66
84
|
{
|
|
67
85
|
"mpxScan": {
|
|
68
|
-
"version": "1.
|
|
86
|
+
"version": "1.3.0",
|
|
69
87
|
"scannedAt": "2026-02-16T22:00:00.000Z",
|
|
70
88
|
"scanDuration": 350
|
|
71
89
|
},
|
|
@@ -90,7 +108,7 @@ Returns structured JSON to stdout (progress/status goes to stderr):
|
|
|
90
108
|
}
|
|
91
109
|
```
|
|
92
110
|
|
|
93
|
-
###
|
|
111
|
+
### Fix Suggestions
|
|
94
112
|
|
|
95
113
|
```bash
|
|
96
114
|
mpx-scan https://example.com --fix nginx
|
|
@@ -99,18 +117,27 @@ mpx-scan https://example.com --fix caddy
|
|
|
99
117
|
mpx-scan https://example.com --fix cloudflare
|
|
100
118
|
```
|
|
101
119
|
|
|
102
|
-
###
|
|
120
|
+
### Brief Output
|
|
103
121
|
|
|
104
122
|
```bash
|
|
105
|
-
mpx-scan https://example.com --
|
|
123
|
+
mpx-scan https://example.com --brief
|
|
106
124
|
```
|
|
107
125
|
|
|
108
|
-
###
|
|
126
|
+
### PDF Export
|
|
109
127
|
|
|
110
128
|
```bash
|
|
111
|
-
|
|
129
|
+
# Generate PDF report (auto-named)
|
|
130
|
+
mpx-scan https://example.com --pdf
|
|
131
|
+
|
|
132
|
+
# Specify output filename
|
|
133
|
+
mpx-scan https://example.com --pdf report.pdf
|
|
134
|
+
|
|
135
|
+
# Combine with JSON output
|
|
136
|
+
mpx-scan https://example.com --json --pdf report.pdf
|
|
112
137
|
```
|
|
113
138
|
|
|
139
|
+
Generates a professional PDF report with color-coded findings, severity grades, and actionable recommendations.
|
|
140
|
+
|
|
114
141
|
### Batch Scanning
|
|
115
142
|
|
|
116
143
|
```bash
|
|
@@ -127,7 +154,38 @@ mpx-scan --schema
|
|
|
127
154
|
|
|
128
155
|
Returns a JSON schema describing all commands, flags, inputs, and outputs โ designed for AI agent tool discovery.
|
|
129
156
|
|
|
130
|
-
|
|
157
|
+
### CLI Reference
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Usage: mpx-scan [url] [options]
|
|
161
|
+
|
|
162
|
+
Arguments:
|
|
163
|
+
url URL to scan
|
|
164
|
+
|
|
165
|
+
Options:
|
|
166
|
+
-V, --version Output version number
|
|
167
|
+
--json Output as structured JSON
|
|
168
|
+
--full Run all checks (Pro only)
|
|
169
|
+
--brief Brief one-line output
|
|
170
|
+
--quiet, -q Minimal output (no banners)
|
|
171
|
+
--no-color Disable ANSI color codes
|
|
172
|
+
--batch Read URLs from stdin (one per line)
|
|
173
|
+
--schema Output JSON schema for tool discovery
|
|
174
|
+
--pdf [filename] Export results as a PDF report
|
|
175
|
+
--fix <platform> Generate fix config (nginx, apache, caddy, cloudflare)
|
|
176
|
+
--timeout <seconds> Connection timeout (default: 10)
|
|
177
|
+
--ci CI mode: exit 1 if below --min-score
|
|
178
|
+
--min-score <score> Minimum score for CI mode (default: 70)
|
|
179
|
+
-h, --help Display help
|
|
180
|
+
|
|
181
|
+
Commands:
|
|
182
|
+
license Show license status
|
|
183
|
+
activate <key> Activate Pro license
|
|
184
|
+
deactivate Return to free tier
|
|
185
|
+
mcp Start MCP stdio server
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## AI Agent Usage
|
|
131
189
|
|
|
132
190
|
mpx-scan is designed to be used by AI agents as well as humans.
|
|
133
191
|
|
|
@@ -151,36 +209,16 @@ The MCP server exposes these tools:
|
|
|
151
209
|
- **`generate_fixes`** โ Scan and generate platform-specific fix config
|
|
152
210
|
- **`get_schema`** โ Get full tool schema
|
|
153
211
|
|
|
154
|
-
### Programmatic Usage
|
|
155
|
-
|
|
156
|
-
```bash
|
|
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
212
|
### Exit Codes
|
|
171
213
|
|
|
172
214
|
| Code | Meaning |
|
|
173
215
|
|------|---------|
|
|
174
|
-
| 0 |
|
|
175
|
-
| 1 |
|
|
176
|
-
| 2 | Invalid arguments |
|
|
177
|
-
| 3 | Configuration error (license, rate limit) |
|
|
178
|
-
| 4 | Network/connectivity error |
|
|
216
|
+
| 0 | Success, no issues found |
|
|
217
|
+
| 1 | Issues found or error |
|
|
218
|
+
| 2 | Invalid usage or bad arguments |
|
|
179
219
|
|
|
180
220
|
### Error Responses (JSON mode)
|
|
181
221
|
|
|
182
|
-
When `--json` is used, errors return structured JSON:
|
|
183
|
-
|
|
184
222
|
```json
|
|
185
223
|
{
|
|
186
224
|
"error": "Description of what went wrong",
|
|
@@ -193,14 +231,11 @@ Error codes: `ERR_NETWORK`, `ERR_SCAN`, `ERR_RATE_LIMIT`, `ERR_PRO_REQUIRED`, `E
|
|
|
193
231
|
### Automation Tips
|
|
194
232
|
|
|
195
233
|
- Use `--json` for machine-parseable output (stdout only, no ANSI)
|
|
196
|
-
- Use `--no-color` to strip ANSI codes from human-readable output
|
|
197
234
|
- Use `--quiet` to suppress banners and progress info
|
|
198
|
-
-
|
|
235
|
+
- Use `--batch --json` for JSONL processing
|
|
199
236
|
- Check exit codes for pass/fail decisions in CI/CD
|
|
200
237
|
|
|
201
|
-
##
|
|
202
|
-
|
|
203
|
-
### CI/CD Integration
|
|
238
|
+
## CI/CD Integration
|
|
204
239
|
|
|
205
240
|
```yaml
|
|
206
241
|
# .github/workflows/security.yml
|
|
@@ -213,38 +248,25 @@ jobs:
|
|
|
213
248
|
- run: npx mpx-scan https://mysite.com --ci --min-score 70 --json
|
|
214
249
|
```
|
|
215
250
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
#!/bin/bash
|
|
220
|
-
for site in site1.com site2.com site3.com; do
|
|
221
|
-
result=$(npx mpx-scan "$site" --json 2>/dev/null)
|
|
222
|
-
grade=$(echo "$result" | jq -r '.score.grade')
|
|
223
|
-
echo "$site: $grade"
|
|
224
|
-
done
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
## ๐ Free vs Pro
|
|
251
|
+
## Free vs Pro
|
|
228
252
|
|
|
229
253
|
| Feature | Free | Pro |
|
|
230
254
|
|---------|------|-----|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
## ๐ License Management
|
|
255
|
+
| Daily scans | 3 | Unlimited |
|
|
256
|
+
| Security headers | โ
| โ
|
|
|
257
|
+
| SSL/TLS checks | โ
| โ
|
|
|
258
|
+
| Server info checks | โ
| โ
|
|
|
259
|
+
| JSON output | โ
| โ
|
|
|
260
|
+
| Batch scanning | โ
| โ
|
|
|
261
|
+
| MCP server | โ
| โ
|
|
|
262
|
+
| DNS security | โ | โ
|
|
|
263
|
+
| Cookie security | โ | โ
|
|
|
264
|
+
| SRI checks | โ | โ
|
|
|
265
|
+
| Exposed files | โ | โ
|
|
|
266
|
+
| Mixed content | โ | โ
|
|
|
267
|
+
| Full scan (`--full`) | โ | โ
|
|
|
268
|
+
|
|
269
|
+
### License Management
|
|
248
270
|
|
|
249
271
|
```bash
|
|
250
272
|
mpx-scan license # Check status
|
|
@@ -252,83 +274,24 @@ mpx-scan activate MPX-PRO-XXXXXXXX # Activate Pro
|
|
|
252
274
|
mpx-scan deactivate # Return to free tier
|
|
253
275
|
```
|
|
254
276
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
```
|
|
258
|
-
Usage: mpx-scan [url] [options]
|
|
259
|
-
|
|
260
|
-
Arguments:
|
|
261
|
-
url URL to scan
|
|
262
|
-
|
|
263
|
-
Options:
|
|
264
|
-
-V, --version Output version number
|
|
265
|
-
--json Output as structured JSON
|
|
266
|
-
--full Run all checks (Pro only)
|
|
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
|
|
272
|
-
--fix <platform> Generate fix config (nginx, apache, caddy, cloudflare)
|
|
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
|
|
277
|
-
|
|
278
|
-
Commands:
|
|
279
|
-
license Show license status
|
|
280
|
-
activate <key> Activate Pro license
|
|
281
|
-
deactivate Return to free tier
|
|
282
|
-
mcp Start MCP stdio server
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
## ๐ฆ Installation
|
|
286
|
-
|
|
287
|
-
```bash
|
|
288
|
-
# Global
|
|
289
|
-
npm install -g mpx-scan
|
|
290
|
-
|
|
291
|
-
# Project dependency
|
|
292
|
-
npm install --save-dev mpx-scan
|
|
293
|
-
|
|
294
|
-
# One-off with npx
|
|
295
|
-
npx mpx-scan https://example.com
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Requirements
|
|
299
|
-
|
|
300
|
-
- Node.js 18.0.0 or higher
|
|
301
|
-
- No native dependencies
|
|
302
|
-
- Works on macOS, Linux, Windows
|
|
303
|
-
|
|
304
|
-
## ๐งช Testing
|
|
305
|
-
|
|
306
|
-
```bash
|
|
307
|
-
npm test
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
## ๐ค Contributing
|
|
311
|
-
|
|
312
|
-
Security improvements and bug fixes are welcome!
|
|
313
|
-
|
|
314
|
-
## ๐ License
|
|
277
|
+
**Upgrade to Pro:** [https://mesaplex.com/mpx-scan](https://mesaplex.com/mpx-scan)
|
|
315
278
|
|
|
316
|
-
|
|
279
|
+
## License
|
|
317
280
|
|
|
318
|
-
See [LICENSE](LICENSE) for full terms.
|
|
281
|
+
Dual License โ Free tier for personal use, Pro license for commercial use and advanced features. See [LICENSE](LICENSE) for full terms.
|
|
319
282
|
|
|
320
|
-
##
|
|
283
|
+
## Links
|
|
321
284
|
|
|
322
|
-
- **Website:** [https://mesaplex.com
|
|
285
|
+
- **Website:** [https://mesaplex.com](https://mesaplex.com)
|
|
323
286
|
- **npm:** [https://www.npmjs.com/package/mpx-scan](https://www.npmjs.com/package/mpx-scan)
|
|
324
287
|
- **GitHub:** [https://github.com/mesaplexdev/mpx-scan](https://github.com/mesaplexdev/mpx-scan)
|
|
325
288
|
- **Support:** support@mesaplex.com
|
|
326
289
|
|
|
327
|
-
|
|
290
|
+
### Related Tools
|
|
328
291
|
|
|
329
|
-
- **mpx-
|
|
330
|
-
- **[mpx-
|
|
331
|
-
- **[mpx-
|
|
292
|
+
- **[mpx-api](https://www.npmjs.com/package/mpx-api)** โ API testing, mocking, and documentation
|
|
293
|
+
- **[mpx-db](https://www.npmjs.com/package/mpx-db)** โ Database management CLI
|
|
294
|
+
- **[mpx-secrets-audit](https://www.npmjs.com/package/mpx-secrets-audit)** โ Secret lifecycle tracking and audit
|
|
332
295
|
|
|
333
296
|
---
|
|
334
297
|
|
package/bin/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ const chalk = require('chalk');
|
|
|
12
12
|
const { scan } = require('../src/index');
|
|
13
13
|
const { formatReport, formatBrief } = require('../src/reporters/terminal');
|
|
14
14
|
const { formatJSON } = require('../src/reporters/json');
|
|
15
|
+
const { generatePDF, getDefaultPDFFilename } = require('../src/reporters/pdf');
|
|
15
16
|
const { generateFixes, PLATFORMS } = require('../src/generators/fixes');
|
|
16
17
|
const { getSchema } = require('../src/schema');
|
|
17
18
|
const {
|
|
@@ -25,13 +26,13 @@ const {
|
|
|
25
26
|
|
|
26
27
|
const pkg = require('../package.json');
|
|
27
28
|
|
|
28
|
-
// Exit codes
|
|
29
|
+
// Exit codes
|
|
29
30
|
const EXIT = {
|
|
30
31
|
SUCCESS: 0, // Success, no issues found
|
|
31
|
-
ISSUES_FOUND: 1, //
|
|
32
|
-
BAD_ARGS: 2, // Invalid arguments
|
|
33
|
-
CONFIG_ERROR:
|
|
34
|
-
NETWORK_ERROR:
|
|
32
|
+
ISSUES_FOUND: 1, // Issues found or error occurred
|
|
33
|
+
BAD_ARGS: 2, // Invalid arguments / bad usage
|
|
34
|
+
CONFIG_ERROR: 1, // Configuration error
|
|
35
|
+
NETWORK_ERROR: 1 // Network/connectivity error
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
// Auto-detect non-interactive mode
|
|
@@ -39,6 +40,12 @@ const isInteractive = process.stdout.isTTY && !process.env.CI;
|
|
|
39
40
|
|
|
40
41
|
const program = new Command();
|
|
41
42
|
|
|
43
|
+
// Error handling โ set before any command/option registration
|
|
44
|
+
program.exitOverride();
|
|
45
|
+
program.configureOutput({
|
|
46
|
+
writeErr: () => {} // Suppress Commander's own error output; we handle it in the catch below
|
|
47
|
+
});
|
|
48
|
+
|
|
42
49
|
program
|
|
43
50
|
.name('mpx-scan')
|
|
44
51
|
.description('Professional website security scanner โ check headers, SSL, DNS, and more')
|
|
@@ -52,6 +59,7 @@ program
|
|
|
52
59
|
.option('--batch', 'Batch mode: read URLs from stdin (one per line)')
|
|
53
60
|
.option('--schema', 'Output JSON schema describing all commands and flags')
|
|
54
61
|
.option('--fix <platform>', `Generate fix config for platform (${PLATFORMS.join(', ')})`)
|
|
62
|
+
.option('--pdf [filename]', 'Export results as a PDF report')
|
|
55
63
|
.option('--timeout <seconds>', 'Connection timeout', '10')
|
|
56
64
|
.option('--ci', 'CI/CD mode: exit 1 if score below threshold')
|
|
57
65
|
.option('--min-score <score>', 'Minimum score for CI mode', '70')
|
|
@@ -71,7 +79,8 @@ program
|
|
|
71
79
|
|
|
72
80
|
// Show help if no URL provided
|
|
73
81
|
if (!url) {
|
|
74
|
-
program.
|
|
82
|
+
program.outputHelp();
|
|
83
|
+
process.exit(EXIT.BAD_ARGS);
|
|
75
84
|
return;
|
|
76
85
|
}
|
|
77
86
|
|
|
@@ -95,7 +104,7 @@ async function runSingleScan(url, options) {
|
|
|
95
104
|
if (jsonMode) {
|
|
96
105
|
console.log(JSON.stringify({ error: `Invalid platform: "${options.fix}". Valid platforms: ${PLATFORMS.join(', ')}`, code: 'ERR_BAD_ARGS' }, null, 2));
|
|
97
106
|
} else {
|
|
98
|
-
console.error(chalk.red
|
|
107
|
+
console.error(chalk.red(`Error: Invalid platform: "${options.fix}"`));
|
|
99
108
|
console.error(chalk.yellow(`Valid platforms: ${PLATFORMS.join(', ')}`));
|
|
100
109
|
console.error('');
|
|
101
110
|
}
|
|
@@ -109,7 +118,7 @@ async function runSingleScan(url, options) {
|
|
|
109
118
|
if (jsonMode) {
|
|
110
119
|
console.log(JSON.stringify({ error: 'Invalid --timeout value. Must be a non-negative number.', code: 'ERR_BAD_ARGS' }, null, 2));
|
|
111
120
|
} else {
|
|
112
|
-
console.error(chalk.red
|
|
121
|
+
console.error(chalk.red('Error: Invalid --timeout value. Must be a non-negative number.'));
|
|
113
122
|
}
|
|
114
123
|
return EXIT.BAD_ARGS;
|
|
115
124
|
}
|
|
@@ -129,7 +138,7 @@ async function runSingleScan(url, options) {
|
|
|
129
138
|
upgrade: 'https://mesaplex.com/mpx-scan'
|
|
130
139
|
}, null, 2));
|
|
131
140
|
} else {
|
|
132
|
-
console.error(chalk.red
|
|
141
|
+
console.error(chalk.red('Error: Daily scan limit reached'));
|
|
133
142
|
console.error(chalk.yellow(`Free tier: ${FREE_DAILY_LIMIT} scans/day`));
|
|
134
143
|
console.error(chalk.gray(`Resets: ${new Date(rateLimit.resetsAt).toLocaleString()}\n`));
|
|
135
144
|
console.error(chalk.blue('Upgrade to Pro for unlimited scans:'));
|
|
@@ -147,7 +156,7 @@ async function runSingleScan(url, options) {
|
|
|
147
156
|
upgrade: 'https://mesaplex.com/mpx-scan'
|
|
148
157
|
}, null, 2));
|
|
149
158
|
} else {
|
|
150
|
-
console.error(chalk.red
|
|
159
|
+
console.error(chalk.red('Error: --full flag requires Pro license'));
|
|
151
160
|
console.error(chalk.yellow('Free tier includes: headers, SSL, server checks'));
|
|
152
161
|
console.error(chalk.yellow('Pro includes: all checks (DNS, cookies, SRI, exposed files, etc.)\n'));
|
|
153
162
|
console.error(chalk.blue('Upgrade: https://mesaplex.com/mpx-scan\n'));
|
|
@@ -184,6 +193,25 @@ async function runSingleScan(url, options) {
|
|
|
184
193
|
console.log(formatReport(results, { ...options, quiet: quietMode }));
|
|
185
194
|
}
|
|
186
195
|
|
|
196
|
+
// Generate PDF if requested
|
|
197
|
+
if (options.pdf !== undefined) {
|
|
198
|
+
const pdfPath = (typeof options.pdf === 'string' && options.pdf)
|
|
199
|
+
? options.pdf
|
|
200
|
+
: getDefaultPDFFilename(results.hostname);
|
|
201
|
+
try {
|
|
202
|
+
await generatePDF(results, pdfPath);
|
|
203
|
+
if (!jsonMode && !options.brief) {
|
|
204
|
+
console.error(chalk.green(`๐ PDF report saved: ${pdfPath}`));
|
|
205
|
+
}
|
|
206
|
+
} catch (pdfErr) {
|
|
207
|
+
if (jsonMode) {
|
|
208
|
+
console.error(JSON.stringify({ warning: `PDF generation failed: ${pdfErr.message}` }));
|
|
209
|
+
} else {
|
|
210
|
+
console.error(chalk.yellow(`Warning: PDF generation failed: ${pdfErr.message}`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
187
215
|
// Check if core scanners errored with network issues (DNS failure, connection refused, etc.)
|
|
188
216
|
const coreScanners = ['headers', 'ssl'];
|
|
189
217
|
const coreErrored = coreScanners.every(name => {
|
|
@@ -203,7 +231,7 @@ async function runSingleScan(url, options) {
|
|
|
203
231
|
if (jsonMode) {
|
|
204
232
|
console.log(JSON.stringify({ error: 'Invalid --min-score value', code: 'ERR_BAD_ARGS' }, null, 2));
|
|
205
233
|
} else {
|
|
206
|
-
console.error(chalk.red
|
|
234
|
+
console.error(chalk.red('Error: Invalid --min-score value. Must be a number.'));
|
|
207
235
|
}
|
|
208
236
|
return EXIT.BAD_ARGS;
|
|
209
237
|
}
|
|
@@ -228,7 +256,7 @@ async function runSingleScan(url, options) {
|
|
|
228
256
|
const code = isNetworkError(err) ? 'ERR_NETWORK' : 'ERR_SCAN';
|
|
229
257
|
console.log(JSON.stringify({ error: err.message, code }, null, 2));
|
|
230
258
|
} else {
|
|
231
|
-
console.error(chalk.red
|
|
259
|
+
console.error(chalk.red('Error:'), err.message);
|
|
232
260
|
console.error('');
|
|
233
261
|
}
|
|
234
262
|
return isNetworkError(err) ? EXIT.NETWORK_ERROR : EXIT.ISSUES_FOUND;
|
|
@@ -244,7 +272,7 @@ async function runBatchMode(options) {
|
|
|
244
272
|
if (jsonMode) {
|
|
245
273
|
console.log(JSON.stringify({ error: 'No URLs provided on stdin', code: 'ERR_NO_INPUT' }, null, 2));
|
|
246
274
|
} else {
|
|
247
|
-
console.error(chalk.red('No URLs provided. Pipe URLs via stdin:'));
|
|
275
|
+
console.error(chalk.red('Error: No URLs provided. Pipe URLs via stdin:'));
|
|
248
276
|
console.error(chalk.gray(' cat urls.txt | mpx-scan --batch --json'));
|
|
249
277
|
}
|
|
250
278
|
process.exit(EXIT.BAD_ARGS);
|
|
@@ -257,7 +285,7 @@ async function runBatchMode(options) {
|
|
|
257
285
|
if (jsonMode) {
|
|
258
286
|
console.log(JSON.stringify({ error: 'No valid URLs found in input', code: 'ERR_NO_INPUT' }, null, 2));
|
|
259
287
|
} else {
|
|
260
|
-
console.error(chalk.red('No valid URLs found in input.'));
|
|
288
|
+
console.error(chalk.red('Error: No valid URLs found in input.'));
|
|
261
289
|
}
|
|
262
290
|
process.exit(EXIT.BAD_ARGS);
|
|
263
291
|
return;
|
|
@@ -391,7 +419,7 @@ program
|
|
|
391
419
|
console.log(chalk.gray(' โข Batch scanning'));
|
|
392
420
|
console.log('');
|
|
393
421
|
} catch (err) {
|
|
394
|
-
console.error(chalk.red
|
|
422
|
+
console.error(chalk.red('Error:'), err.message);
|
|
395
423
|
console.error('');
|
|
396
424
|
process.exit(EXIT.CONFIG_ERROR);
|
|
397
425
|
}
|
|
@@ -474,7 +502,7 @@ program
|
|
|
474
502
|
if (jsonMode) {
|
|
475
503
|
console.log(JSON.stringify({ error: err.message, code: 'ERR_UPDATE' }, null, 2));
|
|
476
504
|
} else {
|
|
477
|
-
console.error(chalk.red
|
|
505
|
+
console.error(chalk.red('Error:'), err.message);
|
|
478
506
|
console.error('');
|
|
479
507
|
}
|
|
480
508
|
process.exit(EXIT.NETWORK_ERROR);
|
|
@@ -503,6 +531,8 @@ ${chalk.bold('Examples:')}
|
|
|
503
531
|
${chalk.cyan('mpx-scan example.com --json')} JSON output
|
|
504
532
|
${chalk.cyan('mpx-scan example.com --fix nginx')} Generate nginx config
|
|
505
533
|
${chalk.cyan('mpx-scan example.com --brief')} One-line summary
|
|
534
|
+
${chalk.cyan('mpx-scan example.com --pdf')} Export PDF report
|
|
535
|
+
${chalk.cyan('mpx-scan example.com --pdf report.pdf')} Export PDF to specific file
|
|
506
536
|
${chalk.cyan('mpx-scan --schema')} Show tool schema (JSON)
|
|
507
537
|
${chalk.cyan('cat urls.txt | mpx-scan --batch --json')} Batch scan from stdin
|
|
508
538
|
${chalk.cyan('mpx-scan mcp')} Start MCP server
|
|
@@ -510,10 +540,8 @@ ${chalk.bold('Examples:')}
|
|
|
510
540
|
|
|
511
541
|
${chalk.bold('Exit Codes:')}
|
|
512
542
|
0 Success, no issues found
|
|
513
|
-
1
|
|
514
|
-
2 Invalid arguments
|
|
515
|
-
3 Configuration error (license, rate limit)
|
|
516
|
-
4 Network/connectivity error
|
|
543
|
+
1 Error or issues found
|
|
544
|
+
2 Invalid usage or bad arguments
|
|
517
545
|
|
|
518
546
|
${chalk.bold('Free vs Pro:')}
|
|
519
547
|
${chalk.yellow('Free:')} 3 scans/day, basic checks (headers, SSL, server)
|
|
@@ -522,4 +550,15 @@ ${chalk.bold('Free vs Pro:')}
|
|
|
522
550
|
${chalk.blue('Upgrade: https://mesaplex.com/mpx-scan')}
|
|
523
551
|
`);
|
|
524
552
|
|
|
525
|
-
|
|
553
|
+
try {
|
|
554
|
+
program.parse();
|
|
555
|
+
} catch (err) {
|
|
556
|
+
if (err.code === 'commander.version') {
|
|
557
|
+
process.exit(0);
|
|
558
|
+
}
|
|
559
|
+
if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed') {
|
|
560
|
+
const msg = err.message.startsWith('error:') ? `Error: ${err.message.slice(7)}` : `Error: ${err.message}`;
|
|
561
|
+
console.error(chalk.red(msg));
|
|
562
|
+
process.exit(EXIT.BAD_ARGS);
|
|
563
|
+
}
|
|
564
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mpx-scan",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Website security scanner CLI. Headers, SSL, cookies, and DNS auditing. AI-native with JSON output and MCP server.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mpx-scan": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node test/run.js"
|
|
11
|
-
"start": "node bin/cli.js"
|
|
10
|
+
"test": "node test/run.js"
|
|
12
11
|
},
|
|
12
|
+
"funding": "https://mesaplex.com/pricing",
|
|
13
13
|
"keywords": [
|
|
14
|
+
"cli",
|
|
15
|
+
"devtools",
|
|
16
|
+
"mesaplex",
|
|
17
|
+
"ai-native",
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"automation",
|
|
21
|
+
"json-output",
|
|
14
22
|
"security",
|
|
15
23
|
"scanner",
|
|
16
24
|
"headers",
|
|
@@ -20,16 +28,10 @@
|
|
|
20
28
|
"owasp",
|
|
21
29
|
"devops",
|
|
22
30
|
"ci-cd",
|
|
23
|
-
"mesaplex",
|
|
24
|
-
"devtools",
|
|
25
31
|
"security-headers",
|
|
26
32
|
"ssl-check",
|
|
27
33
|
"dns-security",
|
|
28
|
-
"cors"
|
|
29
|
-
"mcp",
|
|
30
|
-
"ai-native",
|
|
31
|
-
"model-context-protocol",
|
|
32
|
-
"automation"
|
|
34
|
+
"cors"
|
|
33
35
|
],
|
|
34
36
|
"author": "Mesaplex <support@mesaplex.com>",
|
|
35
37
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -38,20 +40,22 @@
|
|
|
38
40
|
"url": "git+https://github.com/mesaplexdev/mpx-scan.git"
|
|
39
41
|
},
|
|
40
42
|
"homepage": "https://github.com/mesaplexdev/mpx-scan#readme",
|
|
41
|
-
"bugs":
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/mesaplexdev/mpx-scan/issues"
|
|
45
|
+
},
|
|
42
46
|
"engines": {
|
|
43
47
|
"node": ">=18.0.0"
|
|
44
48
|
},
|
|
45
49
|
"dependencies": {
|
|
46
50
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
47
51
|
"chalk": "^4.1.2",
|
|
48
|
-
"commander": "^12.0.0"
|
|
52
|
+
"commander": "^12.0.0",
|
|
53
|
+
"pdfkit": "^0.17.2"
|
|
49
54
|
},
|
|
50
55
|
"files": [
|
|
51
56
|
"src/",
|
|
52
57
|
"bin/",
|
|
53
58
|
"README.md",
|
|
54
|
-
"LICENSE"
|
|
55
|
-
"package.json"
|
|
59
|
+
"LICENSE"
|
|
56
60
|
]
|
|
57
61
|
}
|
package/src/index.js
CHANGED
|
@@ -52,7 +52,7 @@ function checkConnectivity(parsedUrl, timeoutMs) {
|
|
|
52
52
|
path: parsedUrl.pathname || '/',
|
|
53
53
|
method: 'HEAD',
|
|
54
54
|
timeout: timeoutMs,
|
|
55
|
-
headers: { 'User-Agent': 'mpx-scan/1.
|
|
55
|
+
headers: { 'User-Agent': 'mpx-scan/1.3.0 Security Scanner' },
|
|
56
56
|
rejectUnauthorized: false,
|
|
57
57
|
}, (res) => {
|
|
58
58
|
clearTimeout(timer);
|
|
@@ -62,7 +62,7 @@ function checkConnectivity(parsedUrl, timeoutMs) {
|
|
|
62
62
|
|
|
63
63
|
req.on('error', (err) => {
|
|
64
64
|
clearTimeout(timer);
|
|
65
|
-
if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET') {
|
|
65
|
+
if (err.code === 'ENOTFOUND' || err.code === 'EAI_AGAIN' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET') {
|
|
66
66
|
reject(err);
|
|
67
67
|
} else {
|
|
68
68
|
resolve(); // Other errors (like SSL) are fine โ host is reachable
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF Report Generator for mpx-scan
|
|
3
|
+
*
|
|
4
|
+
* Generates professional security scan reports using PDFKit
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const PDFDocument = require('pdfkit');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const pkg = require('../../package.json');
|
|
11
|
+
|
|
12
|
+
// Color palette
|
|
13
|
+
const COLORS = {
|
|
14
|
+
primary: '#1a56db',
|
|
15
|
+
dark: '#1f2937',
|
|
16
|
+
gray: '#6b7280',
|
|
17
|
+
lightGray: '#e5e7eb',
|
|
18
|
+
white: '#ffffff',
|
|
19
|
+
pass: '#16a34a',
|
|
20
|
+
warn: '#ea580c',
|
|
21
|
+
fail: '#dc2626',
|
|
22
|
+
info: '#2563eb',
|
|
23
|
+
error: '#dc2626',
|
|
24
|
+
headerBg: '#1e3a5f',
|
|
25
|
+
sectionBg: '#f3f4f6',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const STATUS_LABELS = {
|
|
29
|
+
pass: 'โ PASS',
|
|
30
|
+
warn: 'โ WARNING',
|
|
31
|
+
fail: 'โ FAIL',
|
|
32
|
+
info: 'โน INFO',
|
|
33
|
+
error: 'โ ERROR',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const SECTION_NAMES = {
|
|
37
|
+
headers: 'Security Headers',
|
|
38
|
+
ssl: 'SSL/TLS',
|
|
39
|
+
cookies: 'Cookies',
|
|
40
|
+
server: 'Server Configuration',
|
|
41
|
+
exposedFiles: 'Exposed Files',
|
|
42
|
+
dns: 'DNS Security',
|
|
43
|
+
sri: 'Subresource Integrity',
|
|
44
|
+
mixedContent: 'Mixed Content',
|
|
45
|
+
redirects: 'Redirects',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate a PDF report from scan results
|
|
50
|
+
* @param {object} results - Scan results object
|
|
51
|
+
* @param {string} outputPath - Path to write the PDF
|
|
52
|
+
* @returns {Promise<string>} - Resolved path of the generated PDF
|
|
53
|
+
*/
|
|
54
|
+
function generatePDF(results, outputPath) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
try {
|
|
57
|
+
const doc = new PDFDocument({
|
|
58
|
+
size: 'A4',
|
|
59
|
+
margins: { top: 50, bottom: 50, left: 50, right: 50 },
|
|
60
|
+
info: {
|
|
61
|
+
Title: `mpx-scan Security Report โ ${results.hostname}`,
|
|
62
|
+
Author: 'mpx-scan',
|
|
63
|
+
Subject: 'Website Security Scan Report',
|
|
64
|
+
Creator: `mpx-scan v${pkg.version}`,
|
|
65
|
+
},
|
|
66
|
+
bufferPages: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const stream = fs.createWriteStream(outputPath);
|
|
70
|
+
doc.pipe(stream);
|
|
71
|
+
|
|
72
|
+
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
73
|
+
const now = new Date().toLocaleDateString('en-US', {
|
|
74
|
+
year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// โโโ Header โโโ
|
|
78
|
+
doc.rect(0, 0, doc.page.width, 100).fill(COLORS.headerBg);
|
|
79
|
+
doc.fontSize(22).fillColor(COLORS.white).font('Helvetica-Bold')
|
|
80
|
+
.text('mpx-scan Security Report', 50, 30);
|
|
81
|
+
doc.fontSize(10).fillColor('#a0b4cc').font('Helvetica')
|
|
82
|
+
.text(`v${pkg.version} โข ${now} โข ${results.url}`, 50, 60);
|
|
83
|
+
|
|
84
|
+
doc.y = 120;
|
|
85
|
+
|
|
86
|
+
// โโโ Summary Box โโโ
|
|
87
|
+
const scorePercent = results.maxScore > 0 ? Math.round((results.score / results.maxScore) * 100) : 0;
|
|
88
|
+
const gradeColor = scorePercent >= 85 ? COLORS.pass : scorePercent >= 55 ? COLORS.warn : COLORS.fail;
|
|
89
|
+
|
|
90
|
+
doc.roundedRect(50, doc.y, pageWidth, 90, 6).fill(COLORS.sectionBg);
|
|
91
|
+
const summaryTop = doc.y + 15;
|
|
92
|
+
|
|
93
|
+
// Grade circle
|
|
94
|
+
doc.circle(100, summaryTop + 30, 28).fill(gradeColor);
|
|
95
|
+
doc.fontSize(26).fillColor(COLORS.white).font('Helvetica-Bold')
|
|
96
|
+
.text(results.grade, 100 - 18, summaryTop + 16, { width: 36, align: 'center' });
|
|
97
|
+
|
|
98
|
+
// Score text
|
|
99
|
+
doc.fontSize(16).fillColor(COLORS.dark).font('Helvetica-Bold')
|
|
100
|
+
.text(`${scorePercent}/100`, 150, summaryTop + 5);
|
|
101
|
+
doc.fontSize(10).fillColor(COLORS.gray).font('Helvetica')
|
|
102
|
+
.text(`${results.score}/${results.maxScore} points โข ${results.scanDuration}ms scan`, 150, summaryTop + 28);
|
|
103
|
+
|
|
104
|
+
// Counts
|
|
105
|
+
const countsX = 360;
|
|
106
|
+
const counts = [
|
|
107
|
+
{ label: 'Passed', count: results.summary.passed, color: COLORS.pass },
|
|
108
|
+
{ label: 'Warnings', count: results.summary.warnings, color: COLORS.warn },
|
|
109
|
+
{ label: 'Failed', count: results.summary.failed, color: COLORS.fail },
|
|
110
|
+
{ label: 'Info', count: results.summary.info, color: COLORS.info },
|
|
111
|
+
];
|
|
112
|
+
counts.forEach((c, i) => {
|
|
113
|
+
const cx = countsX + i * 55;
|
|
114
|
+
doc.fontSize(18).fillColor(c.color).font('Helvetica-Bold')
|
|
115
|
+
.text(String(c.count), cx, summaryTop + 5, { width: 50, align: 'center' });
|
|
116
|
+
doc.fontSize(7).fillColor(COLORS.gray).font('Helvetica')
|
|
117
|
+
.text(c.label, cx, summaryTop + 28, { width: 50, align: 'center' });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
doc.y = summaryTop + 75;
|
|
121
|
+
|
|
122
|
+
// โโโ Detailed Findings โโโ
|
|
123
|
+
for (const [sectionKey, section] of Object.entries(results.sections)) {
|
|
124
|
+
const sectionName = SECTION_NAMES[sectionKey] || sectionKey;
|
|
125
|
+
const sectionPercent = section.maxScore > 0 ? Math.round((section.score / section.maxScore) * 100) : 0;
|
|
126
|
+
|
|
127
|
+
// Check if we need a new page (need at least 100px for header + 1 check)
|
|
128
|
+
if (doc.y > doc.page.height - 150) {
|
|
129
|
+
doc.addPage();
|
|
130
|
+
doc.y = 50;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Section header
|
|
134
|
+
doc.y += 10;
|
|
135
|
+
doc.roundedRect(50, doc.y, pageWidth, 28, 4).fill(COLORS.primary);
|
|
136
|
+
doc.fontSize(11).fillColor(COLORS.white).font('Helvetica-Bold')
|
|
137
|
+
.text(`${sectionName}`, 60, doc.y + 7);
|
|
138
|
+
doc.fontSize(9).fillColor('#c0d4f0').font('Helvetica')
|
|
139
|
+
.text(`${section.grade} โข ${sectionPercent}% (${section.score}/${section.maxScore})`, 60, doc.y + 7, {
|
|
140
|
+
width: pageWidth - 20, align: 'right'
|
|
141
|
+
});
|
|
142
|
+
doc.y += 35;
|
|
143
|
+
|
|
144
|
+
// Checks
|
|
145
|
+
for (const check of section.checks) {
|
|
146
|
+
if (doc.y > doc.page.height - 100) {
|
|
147
|
+
doc.addPage();
|
|
148
|
+
doc.y = 50;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const statusColor = COLORS[check.status] || COLORS.gray;
|
|
152
|
+
const statusLabel = STATUS_LABELS[check.status] || check.status.toUpperCase();
|
|
153
|
+
|
|
154
|
+
// Status badge
|
|
155
|
+
doc.fontSize(7).fillColor(statusColor).font('Helvetica-Bold')
|
|
156
|
+
.text(statusLabel, 60, doc.y);
|
|
157
|
+
|
|
158
|
+
// Check name
|
|
159
|
+
doc.fontSize(10).fillColor(COLORS.dark).font('Helvetica-Bold')
|
|
160
|
+
.text(check.name, 130, doc.y);
|
|
161
|
+
doc.y += 15;
|
|
162
|
+
|
|
163
|
+
// Message
|
|
164
|
+
if (check.message) {
|
|
165
|
+
doc.fontSize(9).fillColor(COLORS.gray).font('Helvetica')
|
|
166
|
+
.text(check.message, 130, doc.y, { width: pageWidth - 90 });
|
|
167
|
+
doc.y += doc.heightOfString(check.message, { width: pageWidth - 90, fontSize: 9 }) + 3;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Recommendation
|
|
171
|
+
if (check.recommendation) {
|
|
172
|
+
doc.fontSize(8).fillColor(COLORS.primary).font('Helvetica-Oblique')
|
|
173
|
+
.text(`โ ${check.recommendation}`, 130, doc.y, { width: pageWidth - 90 });
|
|
174
|
+
doc.y += doc.heightOfString(`โ ${check.recommendation}`, { width: pageWidth - 90, fontSize: 8 }) + 3;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
doc.y += 8;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// โโโ Footer on every page โโโ
|
|
182
|
+
const range = doc.bufferedPageRange();
|
|
183
|
+
for (let i = range.start; i < range.start + range.count; i++) {
|
|
184
|
+
doc.switchToPage(i);
|
|
185
|
+
const footerY = doc.page.height - 35;
|
|
186
|
+
doc.fontSize(7).fillColor(COLORS.gray).font('Helvetica')
|
|
187
|
+
.text(
|
|
188
|
+
`Generated by mpx-scan v${pkg.version} on ${now}`,
|
|
189
|
+
50, footerY, { width: pageWidth, align: 'center' }
|
|
190
|
+
);
|
|
191
|
+
doc.text(
|
|
192
|
+
`Page ${i + 1} of ${range.count}`,
|
|
193
|
+
50, footerY + 12, { width: pageWidth, align: 'center' }
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
doc.end();
|
|
198
|
+
|
|
199
|
+
stream.on('finish', () => resolve(outputPath));
|
|
200
|
+
stream.on('error', reject);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
reject(err);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get default PDF filename for a scan
|
|
209
|
+
* @param {string} hostname - Target hostname
|
|
210
|
+
* @returns {string} Default filename
|
|
211
|
+
*/
|
|
212
|
+
function getDefaultPDFFilename(hostname) {
|
|
213
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
214
|
+
const safeName = hostname.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
215
|
+
return `mpx-scan-report-${safeName}-${date}.pdf`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { generatePDF, getDefaultPDFFilename };
|
package/src/scanners/cookies.js
CHANGED
|
@@ -78,7 +78,7 @@ function fetchCookies(parsedUrl, options = {}) {
|
|
|
78
78
|
const req = protocol.request(parsedUrl.href, {
|
|
79
79
|
method: 'GET',
|
|
80
80
|
timeout,
|
|
81
|
-
headers: { 'User-Agent': 'mpx-scan/1.
|
|
81
|
+
headers: { 'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)' },
|
|
82
82
|
rejectUnauthorized: false,
|
|
83
83
|
}, (res) => {
|
|
84
84
|
// Consume body
|
package/src/scanners/dns.js
CHANGED
|
@@ -117,8 +117,14 @@ async function scanDNS(parsedUrl, options = {}) {
|
|
|
117
117
|
try {
|
|
118
118
|
const mxRecords = await withTimeout(resolver.resolveMx(rootDomain), dnsTimeout, `MX ${rootDomain}`);
|
|
119
119
|
if (mxRecords && mxRecords.length > 0) {
|
|
120
|
-
const mxList = mxRecords.sort((a, b) => a.priority - b.priority).map(r => `${r.exchange} (${r.priority})`);
|
|
121
|
-
|
|
120
|
+
const mxList = mxRecords.sort((a, b) => a.priority - b.priority).map(r => `${r.exchange || '.'} (${r.priority})`);
|
|
121
|
+
// Null MX (RFC 7505): single record with exchange "." and priority 0 means domain does not accept mail
|
|
122
|
+
const isNullMx = mxRecords.length === 1 && (!mxRecords[0].exchange || mxRecords[0].exchange === '.') && mxRecords[0].priority === 0;
|
|
123
|
+
if (isNullMx) {
|
|
124
|
+
checks.push({ name: 'MX Records', status: 'info', message: 'Null MX โ domain does not accept email', value: '. (0)' });
|
|
125
|
+
} else {
|
|
126
|
+
checks.push({ name: 'MX Records', status: 'info', message: `Mail servers: ${mxList.join(', ')}`, value: mxList.join(', ') });
|
|
127
|
+
}
|
|
122
128
|
}
|
|
123
129
|
} catch { /* MX is informational only */ }
|
|
124
130
|
|
|
@@ -151,7 +151,7 @@ function checkPath(parsedUrl, pathStr, options = {}) {
|
|
|
151
151
|
const req = protocol.request(url.href, {
|
|
152
152
|
method: 'GET',
|
|
153
153
|
timeout,
|
|
154
|
-
headers: { 'User-Agent': 'mpx-scan/1.
|
|
154
|
+
headers: { 'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)' },
|
|
155
155
|
rejectUnauthorized: false,
|
|
156
156
|
}, (res) => {
|
|
157
157
|
let body = '';
|
|
@@ -305,7 +305,7 @@ function fetchHeaders(parsedUrl, options = {}) {
|
|
|
305
305
|
|
|
306
306
|
const req = protocol.request(parsedUrl.href, {
|
|
307
307
|
method, timeout,
|
|
308
|
-
headers: { 'User-Agent': 'mpx-scan/1.
|
|
308
|
+
headers: { 'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)' },
|
|
309
309
|
rejectUnauthorized: false,
|
|
310
310
|
}, (res) => {
|
|
311
311
|
if (method === 'GET') { res.resume(); }
|
package/src/scanners/headers.js
CHANGED
|
@@ -191,7 +191,7 @@ function fetchHeaders(parsedUrl, options = {}) {
|
|
|
191
191
|
method,
|
|
192
192
|
timeout,
|
|
193
193
|
headers: {
|
|
194
|
-
'User-Agent': 'mpx-scan/1.
|
|
194
|
+
'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)'
|
|
195
195
|
},
|
|
196
196
|
rejectUnauthorized: false // We check SSL separately
|
|
197
197
|
}, (res) => {
|
|
@@ -86,7 +86,7 @@ function followRedirect(testUrl, options = {}) {
|
|
|
86
86
|
const req = protocol.request(testUrl.href, {
|
|
87
87
|
method: 'GET',
|
|
88
88
|
timeout,
|
|
89
|
-
headers: { 'User-Agent': 'mpx-scan/1.
|
|
89
|
+
headers: { 'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)' },
|
|
90
90
|
rejectUnauthorized: false,
|
|
91
91
|
}, (res) => {
|
|
92
92
|
res.resume(); // Consume body
|
package/src/scanners/server.js
CHANGED
|
@@ -88,7 +88,7 @@ function checkRedirectToHttps(httpUrl, options = {}) {
|
|
|
88
88
|
const req = http.request(httpUrl.href, {
|
|
89
89
|
method,
|
|
90
90
|
timeout,
|
|
91
|
-
headers: { 'User-Agent': 'mpx-scan/1.
|
|
91
|
+
headers: { 'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)' },
|
|
92
92
|
}, (res) => {
|
|
93
93
|
if (method === 'GET') { res.resume(); }
|
|
94
94
|
if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
@@ -115,7 +115,7 @@ function fetchWithOrigin(parsedUrl, options = {}) {
|
|
|
115
115
|
method,
|
|
116
116
|
timeout,
|
|
117
117
|
headers: {
|
|
118
|
-
'User-Agent': 'mpx-scan/1.
|
|
118
|
+
'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)',
|
|
119
119
|
'Origin': 'https://evil.example.com'
|
|
120
120
|
},
|
|
121
121
|
rejectUnauthorized: false,
|
|
@@ -141,7 +141,7 @@ function checkMethods(parsedUrl, options = {}) {
|
|
|
141
141
|
const req = protocol.request(parsedUrl.href, {
|
|
142
142
|
method: 'OPTIONS',
|
|
143
143
|
timeout,
|
|
144
|
-
headers: { 'User-Agent': 'mpx-scan/1.
|
|
144
|
+
headers: { 'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)' },
|
|
145
145
|
rejectUnauthorized: false,
|
|
146
146
|
}, (res) => {
|
|
147
147
|
const allow = res.headers.allow || '';
|
package/src/scanners/sri.js
CHANGED
|
@@ -125,7 +125,7 @@ function fetchBody(parsedUrl, options = {}) {
|
|
|
125
125
|
method: 'GET',
|
|
126
126
|
timeout,
|
|
127
127
|
headers: {
|
|
128
|
-
'User-Agent': 'mpx-scan/1.
|
|
128
|
+
'User-Agent': 'mpx-scan/1.3.0 Security Scanner (https://github.com/mesaplexdev/mpx-scan)',
|
|
129
129
|
'Accept': 'text/html'
|
|
130
130
|
},
|
|
131
131
|
rejectUnauthorized: false,
|
package/src/schema.js
CHANGED
|
@@ -194,6 +194,40 @@ function getSchema() {
|
|
|
194
194
|
free: SCANNER_TIERS.free,
|
|
195
195
|
pro: SCANNER_TIERS.pro
|
|
196
196
|
},
|
|
197
|
+
exitCodes: {
|
|
198
|
+
0: 'Success / no issues',
|
|
199
|
+
1: 'Error or security issues found'
|
|
200
|
+
},
|
|
201
|
+
globalFlags: {
|
|
202
|
+
'--json': {
|
|
203
|
+
type: 'boolean',
|
|
204
|
+
default: false,
|
|
205
|
+
description: 'Output results as structured JSON'
|
|
206
|
+
},
|
|
207
|
+
'--quiet': {
|
|
208
|
+
type: 'boolean',
|
|
209
|
+
default: false,
|
|
210
|
+
description: 'Suppress non-essential output (no banners or progress)'
|
|
211
|
+
},
|
|
212
|
+
'--no-color': {
|
|
213
|
+
type: 'boolean',
|
|
214
|
+
default: false,
|
|
215
|
+
description: 'Disable ANSI color codes in output'
|
|
216
|
+
},
|
|
217
|
+
'--schema': {
|
|
218
|
+
type: 'boolean',
|
|
219
|
+
default: false,
|
|
220
|
+
description: 'Output this schema as JSON'
|
|
221
|
+
},
|
|
222
|
+
'--version': {
|
|
223
|
+
type: 'boolean',
|
|
224
|
+
description: 'Show version number'
|
|
225
|
+
},
|
|
226
|
+
'--help': {
|
|
227
|
+
type: 'boolean',
|
|
228
|
+
description: 'Show help information'
|
|
229
|
+
}
|
|
230
|
+
},
|
|
197
231
|
mcpConfig: {
|
|
198
232
|
description: 'Add to your MCP client configuration to use mpx-scan as an AI tool',
|
|
199
233
|
config: {
|