correctover-scan 2.1.3 → 3.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/index.js CHANGED
@@ -1,20 +1,653 @@
1
+ #!/usr/bin/env node
1
2
  /**
2
- * correctover-scan v2 compatibility shim.
3
- * Forwards to maintained correctover runtime.
3
+ * correctover-scan — CCS Security Scanner for MCP Configurations
4
+ * Usage: npx correctover-scan [config-file] [options]
5
+ *
6
+ * Scans MCP configuration files for security issues.
7
+ * Maps to OWASP AISVS 1.0 and Chinese National Standard《智能体应用安全基本要求》
4
8
  */
5
- 'use strict';
6
9
 
7
- const correctover = require('correctover');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { runScan, parseConfig, KNOWN_CONFIG_PATHS } = require('./core/scanner');
13
+ const { runBundleScan, discoverBundleFiles } = require('./core/bundle-scanner');
14
+ const { recordCall, getUpgradeMessage } = require('./core/license');
15
+ const skillpay = require('./core/skillpay');
16
+ const { formatHTMLReport, normalizeConfigFindings, normalizeBundleFindings, failHint } = require('./core/report');
8
17
 
9
- const migrationNotice = [
10
- 'correctover-scan v1.x has been retired.',
11
- 'Use correctover for maintained MCP/tool-call scanning:',
12
- ' npm install correctover',
13
- ' const { MCPSecurityValidator } = require("correctover");'
14
- ].join('\n');
18
+ const VERSION = '3.1.0';
19
+ const PRODUCT = 'correctover-scan';
20
+ const JS_EXT = new Set(['.js', '.mjs', '.cjs', '.ts']);
15
21
 
16
- module.exports = {
17
- ...correctover,
18
- migrationNotice,
19
- MIGRATION_NOTICE: migrationNotice
22
+ // Colors
23
+ const c = {
24
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
25
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
26
+ cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
27
+ bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m',
20
28
  };
29
+
30
+ const icons = { pass: '✅', warn: '⚠️', fail: '❌', info: 'ℹ️' };
31
+ const sevColors = { critical: c.red, high: c.yellow, medium: c.cyan, low: c.gray };
32
+ const sevLabels = { critical: 'CRITICAL', high: 'HIGH', medium: 'MEDIUM', low: 'LOW' };
33
+
34
+ function printBanner(stream) {
35
+ const out = stream === 'stderr' ? console.error : console.log;
36
+ out(`
37
+ ${c.bold}${c.blue} ╔══════════════════════════════════════════╗
38
+ ║ CCS Security Scanner by Correctover ║
39
+ ║ AI Agent Runtime Assurance ║
40
+ ╚══════════════════════════════════════════╝${c.reset}
41
+ ${c.dim}v${VERSION} | OWASP AISVS 1.0 | 14 checks · MCP config + JS bundle modes${c.reset}
42
+ `);
43
+ }
44
+
45
+ function findConfigFiles(dir) {
46
+ const found = [];
47
+ for (const relPath of KNOWN_CONFIG_PATHS) {
48
+ const fullPath = path.resolve(dir, relPath);
49
+ if (fs.existsSync(fullPath)) {
50
+ found.push(fullPath);
51
+ }
52
+ }
53
+ // Also scan for any mcp*.json or mcp*.yaml in common locations
54
+ const scanDirs = ['.', '.cursor', '.claude', '.vscode', '.mcp', 'config'];
55
+ for (const d of scanDirs) {
56
+ const dirPath = path.resolve(dir, d);
57
+ if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
58
+ try {
59
+ const files = fs.readdirSync(dirPath);
60
+ for (const f of files) {
61
+ if (/^mcp[_-]?.*\.(json|yaml|yml)$/.test(f)) {
62
+ const fp = path.join(dirPath, f);
63
+ if (!found.includes(fp)) found.push(fp);
64
+ }
65
+ }
66
+ } catch (e) {}
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+
72
+ function formatResults(results, stats, filename) {
73
+ const lines = [];
74
+
75
+ // File header
76
+ lines.push(`${c.bold}📄 ${filename}${c.reset}`);
77
+ lines.push('─'.repeat(50));
78
+
79
+ // Score
80
+ const scoreColor = stats.score >= 80 ? c.green : stats.score >= 60 ? c.yellow : c.red;
81
+ lines.push(`\n${c.bold}Security Score: ${scoreColor}${c.bold}${stats.score}/100${c.reset}`);
82
+ lines.push(` ${c.green}✓ ${stats.pass} passed${c.reset} ${c.yellow}⚠ ${stats.warn} warnings${c.reset} ${c.red}✗ ${stats.fail} critical${c.reset} ${c.blue}ℹ ${stats.info} info${c.reset}\n`);
83
+
84
+ // Group by category
85
+ const categories = {};
86
+ for (const r of results) {
87
+ if (!categories[r.category]) categories[r.category] = [];
88
+ categories[r.category].push(r);
89
+ }
90
+
91
+ for (const [cat, checks] of Object.entries(categories)) {
92
+ lines.push(`${c.dim}── ${cat} ──${c.reset}`);
93
+ for (const r of checks) {
94
+ const icon = icons[r.status];
95
+ const sevColor = sevColors[r.severity];
96
+ const sevLabel = sevLabels[r.severity];
97
+ const statusStr = r.status === 'pass' ? c.green + 'PASS' : r.status === 'fail' ? c.red + 'FAIL' : r.status === 'warn' ? c.yellow + 'WARN' : c.blue + 'INFO';
98
+ lines.push(` ${icon} ${r.name} ${c.reset}[${statusStr}${c.reset}] ${c.dim}${r.aisvs}${c.reset}`);
99
+ }
100
+ lines.push('');
101
+ }
102
+
103
+ // Recommendations
104
+ const issues = results.filter(r => r.status === 'fail' || r.status === 'warn');
105
+ if (issues.length > 0) {
106
+ lines.push(`${c.bold}${c.yellow}Recommendations:${c.reset}\n`);
107
+ for (const r of issues) {
108
+ const icon = r.status === 'fail' ? '🔴' : '🟡';
109
+ lines.push(` ${icon} ${c.bold}${r.name}${c.reset}`);
110
+ lines.push(` ${c.gray}${r.fix}${c.reset}\n`);
111
+ }
112
+ } else {
113
+ lines.push(`${c.green}${c.bold}🎉 All checks passed! Your MCP configuration is secure.${c.reset}\n`);
114
+ }
115
+
116
+ // CTA
117
+ lines.push('');
118
+ lines.push(`${c.dim}── Upgrade ─────────────────────────────────${c.reset}`);
119
+ if (stats.fail > 0 || stats.score < 60) {
120
+ lines.push(`${c.yellow}${c.bold}⚡ Score too low? CCS Pro generates formal compliance reports.${c.reset}`);
121
+ lines.push(`${c.dim} Enterprise: real-time runtime protection + Token guarantee${c.reset}`);
122
+ lines.push(`${c.cyan} → Upgrade: https://correctover.com/checkout${c.reset}`);
123
+ } else {
124
+ lines.push(`${c.green}✓ Good score! Get a formal compliance certificate with CCS Pro.${c.reset}`);
125
+ lines.push(`${c.dim} Audit reports · Team dashboard · Custom rules · SOC 2 ready${c.reset}`);
126
+ lines.push(`${c.cyan} → https://correctover.com/checkout${c.reset}`);
127
+ }
128
+ lines.push(`${c.dim}Enterprise: runtime SDK + Token guarantee → https://correctover.com${c.reset}`);
129
+ lines.push('');
130
+ lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/DSHCorrectover${c.reset}`);
131
+
132
+ // Manual audit CTA
133
+ lines.push('');
134
+ lines.push(`${c.dim}Free automated scan covers surface-level checks. A manual Correctover audit goes deeper: 116 semantic intent rules, 5-day turnaround, findings grounded in real MCP ecosystem CVEs. First customers: if we find no critical-severity issue, you pay nothing. \u2192 ${c.cyan}https://dshcorrectover.github.io/agent-audit/${c.reset}`);
135
+
136
+ const cnBlock = skillpay.block('scan');
137
+ if (cnBlock) {
138
+ lines.push('');
139
+ lines.push(cnBlock);
140
+ }
141
+
142
+ return lines.join('\n');
143
+ }
144
+
145
+ /* ------------------------------------------------------------------ */
146
+ /* Bundle mode output */
147
+ /* ------------------------------------------------------------------ */
148
+
149
+ function formatBundleResults(bundle, baseLabel) {
150
+ const lines = [];
151
+ const stats = bundle.stats;
152
+ lines.push(`${c.bold}📦 Bundle/code scan: ${baseLabel}${c.reset}`);
153
+ lines.push(` ${c.dim}files: ${bundle.files.length} · code-layer checks: ${bundle.checks.length} (12 automatic verdicts + 5 semi-automatic signal enums; check ids continue the 14-check config scheme)${c.reset}`);
154
+ lines.push('─'.repeat(50));
155
+
156
+ const scoreColor = stats.score >= 80 ? c.green : stats.score >= 60 ? c.yellow : c.red;
157
+ lines.push(`\n${c.bold}Security Score: ${scoreColor}${c.bold}${stats.score}/100${c.reset}`);
158
+ lines.push(` ${c.green}✓ ${stats.pass} checks passed${c.reset} ${c.yellow}⚠ ${stats.warn} checks with warnings${c.reset} ${c.red}✗ ${stats.fail} checks with critical findings${c.reset} ${c.blue}ℹ ${stats.info} checks info-only${c.reset}`);
159
+ lines.push(` ${c.dim}findings: ${stats.findings.fail} fail · ${stats.findings.warn} warn · ${stats.findings.info} info · ${stats.findings.suppressed} suppressed by context heuristics${c.reset}\n`);
160
+
161
+ for (const fr of bundle.files) {
162
+ const active = [];
163
+ const suppressedCount = fr.results.reduce((n, r) => n + r.findings.filter(f => f.suppressed).length, 0);
164
+ for (const r of fr.results) {
165
+ for (const f of r.findings) {
166
+ if (!f.suppressed) active.push({ check: r, f });
167
+ }
168
+ }
169
+ if (active.length === 0) {
170
+ lines.push(`${c.green}✅ ${fr.file} — no fail/warn findings (${suppressedCount} known-benign signals suppressed, see JSON for detail)${c.reset}\n`);
171
+ continue;
172
+ }
173
+ lines.push(`${c.bold}📄 ${fr.file}${c.reset}`);
174
+ for (const { check, f } of active) {
175
+ const icon = icons[f.severity === 'fail' ? 'fail' : f.severity === 'warn' ? 'warn' : 'info'];
176
+ const statusStr = f.severity === 'fail' ? c.red + 'FAIL' : f.severity === 'warn' ? c.yellow + 'WARN' : c.blue + 'INFO';
177
+ lines.push(` ${icon} ${c.dim}[${check.id} · ${check.aisvs}]${c.reset} ${statusStr}${c.reset} ${c.bold}L${f.line}${c.reset}`);
178
+ lines.push(` ${f.message}`);
179
+ if (f.snippet) lines.push(` ${c.gray}${f.snippet}${c.reset}`);
180
+ lines.push('');
181
+ }
182
+ }
183
+
184
+ // Semi-automatic checks summary
185
+ const semiIds = ['budget-limit', 'logging', 'version-pin', 'error-handling', 'output-validation'];
186
+ lines.push(`${c.dim}── Semi-automatic checks (signals enumerated; conclusion needs manual review) ──${c.reset}`);
187
+ for (const id of semiIds) {
188
+ let n = 0;
189
+ for (const fr of bundle.files) {
190
+ const r = fr.results.find(x => x.id === id);
191
+ if (r) n += r.findings.length;
192
+ }
193
+ lines.push(` ${c.blue}ℹ${c.reset} ${c.dim}${id}: ${n} signal(s) listed in JSON output${c.reset}`);
194
+ }
195
+
196
+ lines.push('');
197
+ lines.push(`${c.dim}Note: bundle mode is signal scanning over published/minified code. It locates every${c.reset}`);
198
+ lines.push(`${c.dim}risk-bearing string/API call with file+line; it does not prove reachability or intent —${c.reset}`);
199
+ lines.push(`${c.dim}fail/warn items and the semi-automatic signals are the input for manual deep review.${c.reset}`);
200
+
201
+ lines.push('');
202
+ lines.push(`${c.dim}── Upgrade ─────────────────────────────────${c.reset}`);
203
+ if (stats.fail > 0 || stats.score < 60) {
204
+ lines.push(`${c.yellow}${c.bold}⚡ Critical findings present. CCS Pro generates formal compliance reports.${c.reset}`);
205
+ lines.push(`${c.dim} Enterprise: real-time runtime protection + Token guarantee${c.reset}`);
206
+ lines.push(`${c.cyan} → Upgrade: https://correctover.com/checkout${c.reset}`);
207
+ } else {
208
+ lines.push(`${c.green}✓ No critical signals! Get a formal compliance certificate with CCS Pro.${c.reset}`);
209
+ lines.push(`${c.cyan} → https://correctover.com/checkout${c.reset}`);
210
+ }
211
+ lines.push(`${c.dim}Manual bundle audit (116-rule deep review): https://dshcorrectover.github.io/agent-audit/${c.reset}`);
212
+ lines.push('');
213
+ lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/DSHCorrectover${c.reset}`);
214
+
215
+ const cnBundleBlock = skillpay.block('scan');
216
+ if (cnBundleBlock) {
217
+ lines.push('');
218
+ lines.push(cnBundleBlock);
219
+ }
220
+ return lines.join('\n');
221
+ }
222
+
223
+ function formatBundleJSON(bundle, target) {
224
+ return JSON.stringify({
225
+ scanner: 'correctover-scan', version: VERSION, mode: 'bundle', target,
226
+ stats: bundle.stats,
227
+ files: bundle.files.map(fr => ({
228
+ file: fr.file,
229
+ results: fr.results.map(r => ({
230
+ id: r.id, name: r.name, category: r.category, aisvs: r.aisvs,
231
+ severity: r.severity, status: r.status, fix: r.fix,
232
+ findings: r.findings.map(f => ({ line: f.line, severity: f.severity, suppressed: f.suppressed, message: f.message, snippet: f.snippet })),
233
+ })),
234
+ })),
235
+ }, null, 2);
236
+ }
237
+
238
+ function formatBundleSARIF(bundle, target) {
239
+ const rules = [];
240
+ const results = [];
241
+ for (const fr of bundle.files) {
242
+ for (const r of fr.results) {
243
+ if (!rules.some(x => x.id === r.id)) {
244
+ rules.push({
245
+ id: r.id, name: r.name,
246
+ shortDescription: { text: `${r.category}: ${r.name}` },
247
+ helpUri: `https://correctover.com/scan/#check-${r.id}`,
248
+ properties: { aisvs: r.aisvs, severity: r.severity, mode: 'bundle' },
249
+ });
250
+ }
251
+ for (const f of r.findings) {
252
+ if (f.suppressed || f.severity === 'info') continue;
253
+ results.push({
254
+ ruleId: r.id,
255
+ level: f.severity === 'fail' ? 'error' : 'warning',
256
+ message: { text: f.message },
257
+ locations: [{ physicalLocation: {
258
+ artifactLocation: { uri: fr.file },
259
+ region: f.line ? { startLine: f.line } : undefined,
260
+ } }],
261
+ });
262
+ }
263
+ }
264
+ }
265
+ return {
266
+ version: '2.1.0',
267
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
268
+ runs: [{
269
+ tool: { driver: { name: 'correctover-scan', version: VERSION, informationUri: 'https://correctover.com', rules } },
270
+ results,
271
+ }],
272
+ };
273
+ }
274
+
275
+ function formatSARIF(results, filename) {
276
+ return {
277
+ version: '2.1.0',
278
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
279
+ runs: [{
280
+ tool: {
281
+ driver: {
282
+ name: 'correctover-scan',
283
+ version: VERSION,
284
+ informationUri: 'https://correctover.com',
285
+ rules: results.map(r => ({
286
+ id: r.id,
287
+ name: r.name,
288
+ shortDescription: { text: `${r.category}: ${r.name}` },
289
+ helpUri: `https://correctover.com/scan/#check-${r.id}`,
290
+ properties: { aisvs: r.aisvs, severity: r.severity }
291
+ }))
292
+ }
293
+ },
294
+ results: results.filter(r => r.status === 'fail' || r.status === 'warn').map(r => ({
295
+ ruleId: r.id,
296
+ level: r.status === 'fail' ? 'error' : 'warning',
297
+ message: { text: r.fix },
298
+ locations: [{ physicalLocation: { artifactLocation: { uri: filename } } }]
299
+ }))
300
+ }]
301
+ };
302
+ }
303
+
304
+ function formatJSON(results, stats, filename) {
305
+ return JSON.stringify({ scanner: 'correctover-scan', version: VERSION, file: filename, stats, results }, null, 2);
306
+ }
307
+
308
+ /* ------------------------------------------------------------------ */
309
+ /* HTML shareable report (verification-as-acquisition funnel) */
310
+ /* ------------------------------------------------------------------ */
311
+
312
+ function defaultReportPath() {
313
+ const d = new Date();
314
+ const pad = (n) => String(n).padStart(2, '0');
315
+ const base = `correctover-scan-report-${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
316
+ let p = path.join(process.cwd(), `${base}.html`);
317
+ let n = 2;
318
+ while (fs.existsSync(p)) p = path.join(process.cwd(), `${base}-${n++}.html`);
319
+ return p;
320
+ }
321
+
322
+ function runBundleMode(target, format, outFile) {
323
+ // All diagnostics/banners go to stderr so that stdout carries only the
324
+ // machine-readable payload for json/sarif consumers.
325
+ const diag = (...a) => console.error(...a);
326
+ printBanner(format === 'text' && !outFile ? 'stdout' : 'stderr');
327
+
328
+ // License check (same 50/day free tier counting as config mode)
329
+ const status = recordCall(PRODUCT);
330
+ if (!status.authorized) {
331
+ diag(getUpgradeMessage(status));
332
+ process.exit(1);
333
+ }
334
+ if (status.tier === 'free') {
335
+ diag(`${c.dim}📊 Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
336
+ diag(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}`);
337
+ const cnLine = skillpay.ctaLine('scan');
338
+ if (cnLine) diag(`${c.dim}${cnLine}${c.reset}`);
339
+ diag('');
340
+ } else if (status.tier === 'pro') {
341
+ diag(`${c.green}✅ Pro license active — unlimited scans${c.reset}\n`);
342
+ }
343
+
344
+ if (!fs.existsSync(target)) {
345
+ diag(`${c.red}Error: path not found: ${target}${c.reset}`);
346
+ process.exit(1);
347
+ }
348
+
349
+ const abs = path.resolve(target);
350
+ const isDir = fs.statSync(abs).isDirectory();
351
+ const baseDir = isDir ? abs : path.dirname(abs);
352
+ let files;
353
+ try {
354
+ files = discoverBundleFiles(abs);
355
+ } catch (e) {
356
+ diag(`${c.red}Error discovering bundle files: ${e.message}${c.reset}`);
357
+ process.exit(1);
358
+ }
359
+ if (files.length === 0) {
360
+ diag(`${c.yellow}No JavaScript files (.js/.mjs/.cjs) found under ${target}.${c.reset}`);
361
+ process.exit(0);
362
+ }
363
+ diag(`${c.dim}Bundle mode: ${files.length} JS file(s) under ${target}${c.reset}\n`);
364
+
365
+ const bundle = runBundleScan(files, baseDir);
366
+ const label = path.relative(process.cwd(), abs) || abs;
367
+
368
+ let payload;
369
+ let htmlDefault = false;
370
+ if (format === 'json') {
371
+ payload = formatBundleJSON(bundle, label);
372
+ } else if (format === 'sarif') {
373
+ payload = JSON.stringify(formatBundleSARIF(bundle, label), null, 2);
374
+ } else if (format === 'html') {
375
+ const labelCounts = {
376
+ pass: bundle.stats.pass, warn: bundle.stats.warn, fail: bundle.stats.fail, info: bundle.stats.info,
377
+ score: bundle.stats.score,
378
+ };
379
+ payload = formatHTMLReport({
380
+ mode: 'bundle/code scan', target: label,
381
+ items: normalizeBundleFindings(bundle), totals: labelCounts,
382
+ checksLabel: '12 automatic + 5 semi-automatic code-layer checks',
383
+ version: VERSION,
384
+ });
385
+ if (!outFile) { outFile = defaultReportPath(); htmlDefault = true; }
386
+ } else {
387
+ payload = formatBundleResults(bundle, label);
388
+ }
389
+ if (outFile) {
390
+ try {
391
+ fs.writeFileSync(outFile, payload + '\n');
392
+ diag(`${c.green}✅ Report written to ${outFile}${c.reset}`);
393
+ if (htmlDefault) {
394
+ diag(`${c.dim}Shareable HTML report — forward it to your team. ${failHint(bundle.stats.findings.fail)}${c.reset}`);
395
+ }
396
+ } catch (e) {
397
+ console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
398
+ process.exit(1);
399
+ }
400
+ } else {
401
+ console.log(payload);
402
+ }
403
+
404
+ // Exit code: 1 if any check has fail-level findings
405
+ if (bundle.stats.findings.fail > 0) process.exit(1);
406
+ }
407
+
408
+ function main() {
409
+ const args = process.argv.slice(2);
410
+ let configPath = null;
411
+ let bundlePath = null;
412
+ let format = 'text'; // text, json, sarif
413
+ let scanDir = process.cwd();
414
+ let recursive = false;
415
+ let outFile = null;
416
+
417
+ // Parse args
418
+ for (let i = 0; i < args.length; i++) {
419
+ const arg = args[i];
420
+ if (arg === '--version' || arg === '-v') {
421
+ console.log(`correctover-scan v${VERSION}`);
422
+ process.exit(0);
423
+ } else if (arg === '--help' || arg === '-h') {
424
+ console.log(`
425
+ Usage: correctover-scan [target] [options]
426
+
427
+ MCP config mode (default for .json/.yaml/mcp.json):
428
+ correctover-scan mcp.json Scan a specific MCP config
429
+ correctover-scan -d ./project Auto-detect configs in directory
430
+ correctover-scan -d ./project -r Recursively find config files
431
+
432
+ Bundle / published-package code mode (v1.4.0+):
433
+ correctover-scan --bundle ./pkg Audit an unpacked npm package
434
+ (reads package.json entry + all .js/.mjs/.cjs)
435
+ correctover-scan --bundle dist/app.min.js Audit a single minified/bundled JS file
436
+ correctover-scan ./cli.js Auto-detected: .js/.mjs/.cjs target → bundle mode
437
+ correctover-scan --bundle ./pkg -f sarif SARIF with file+line locations
438
+
439
+ Options:
440
+ --bundle <path> Explicitly run bundle/code scan on a JS file or directory
441
+ -f, --format <type> Output format: text, json, sarif, html (default: text)
442
+ html writes a self-contained shareable report file
443
+ (correctover-scan-report-<date>.html if no -o given)
444
+ -d, --dir <path> Directory to scan for MCP configs (default: cwd)
445
+ -r, --recursive Recursively find MCP config files
446
+ -o, --output <file> Write the report to a file (text/json/sarif) instead of stdout
447
+ -v, --version Show version
448
+ -h, --help Show this help
449
+
450
+ Bundle mode performs signal scanning of shipped/minified code (hardcoded secrets,
451
+ plaintext endpoints, cloud metadata/SSRF, shell:true/exec, eval/Function/vm, env
452
+ credential flow, permission gates, MCP transport auth, timeouts, kill switch,
453
+ sandbox, input validation) with context-based false-positive suppression. It
454
+ locates signals at file+line; reachability/intent conclusions need manual review.
455
+
456
+ Examples:
457
+ npx correctover-scan Auto-detect MCP configs in cwd
458
+ npx correctover-scan --bundle . Audit the current package's code
459
+ `);
460
+ process.exit(0);
461
+ } else if (arg === '--format' || arg === '-f') {
462
+ format = args[++i];
463
+ } else if (arg === '--dir' || arg === '-d') {
464
+ scanDir = args[++i];
465
+ } else if (arg === '--recursive' || arg === '-r') {
466
+ recursive = true;
467
+ } else if (arg === '--bundle' || arg === '-b') {
468
+ bundlePath = args[++i];
469
+ } else if (arg === '--output' || arg === '-o') {
470
+ outFile = args[++i] || null;
471
+ } else if (!arg.startsWith('-')) {
472
+ configPath = arg;
473
+ }
474
+ }
475
+
476
+ // Auto-detect bundle mode: positional .js/.mjs/.cjs file, or a directory
477
+ // that contains JS but no MCP config and looks like a package.
478
+ if (!bundlePath && configPath && fs.existsSync(configPath)) {
479
+ const st = fs.statSync(configPath);
480
+ if (st.isFile() && JS_EXT.has(path.extname(configPath).toLowerCase())) {
481
+ bundlePath = configPath;
482
+ configPath = null;
483
+ } else if (st.isDirectory() && !configPath.endsWith('.json')) {
484
+ // directory positional: prefer config mode only if a known config exists
485
+ const hasConfig = KNOWN_CONFIG_PATHS.some(rel => fs.existsSync(path.join(configPath, rel)));
486
+ if (!hasConfig) {
487
+ let hasJS = false;
488
+ try { hasJS = fs.readdirSync(configPath).some(f => JS_EXT.has(path.extname(f).toLowerCase())); } catch (e) {}
489
+ if (hasJS || fs.existsSync(path.join(configPath, 'package.json'))) {
490
+ bundlePath = configPath;
491
+ configPath = null;
492
+ }
493
+ }
494
+ }
495
+ }
496
+
497
+ if (bundlePath) {
498
+ return runBundleMode(bundlePath, format, outFile);
499
+ }
500
+
501
+ // In json/sarif mode all diagnostics go to stderr, keeping stdout clean.
502
+ const diag = (format === 'text' && !outFile) ? console.log : (...a) => console.error(...a);
503
+ printBanner(format === 'text' && !outFile ? 'stdout' : 'stderr');
504
+
505
+ // Text-mode payload buffer: when -o writes a report file, the report text is
506
+ // collected instead of being streamed to stdout (diagnostics stay on stderr).
507
+ const textBuffer = outFile ? [] : null;
508
+ const emit = textBuffer ? (s) => textBuffer.push(s) : (s) => console.log(s);
509
+
510
+ // License check
511
+ const status = recordCall(PRODUCT);
512
+ if (!status.authorized) {
513
+ diag(getUpgradeMessage(status));
514
+ process.exit(1);
515
+ }
516
+ if (status.tier === 'free') {
517
+ diag(`${c.dim}📊 Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
518
+ diag(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}`);
519
+ const cnLine = skillpay.ctaLine('scan');
520
+ if (cnLine) diag(`${c.dim}${cnLine}${c.reset}`);
521
+ diag('');
522
+ } else if (status.tier === 'pro') {
523
+ diag(`${c.green}✅ Pro license active — unlimited scans${c.reset}\n`);
524
+ }
525
+
526
+ const filesToScan = [];
527
+
528
+ if (configPath) {
529
+ // Specific file
530
+ if (!fs.existsSync(configPath)) {
531
+ console.error(`${c.red}Error: File not found: ${configPath}${c.reset}`);
532
+ process.exit(1);
533
+ }
534
+ filesToScan.push(configPath);
535
+ } else {
536
+ // Auto-detect
537
+ diag(`${c.dim}Auto-detecting MCP configs in ${scanDir}...${c.reset}\n`);
538
+ const found = findConfigFiles(scanDir);
539
+ if (recursive) {
540
+ // Walk directory tree
541
+ function walkDir(dir) {
542
+ try {
543
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
544
+ for (const entry of entries) {
545
+ if (entry.name.startsWith('.') && entry.name !== '.cursor' && entry.name !== '.claude' && entry.name !== '.vscode' && entry.name !== '.mcp') continue;
546
+ const fullPath = path.join(dir, entry.name);
547
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
548
+ walkDir(fullPath);
549
+ } else if (entry.isFile() && /mcp[_-]?.*\.(json|yaml|yml)$/.test(entry.name)) {
550
+ if (!found.includes(fullPath)) found.push(fullPath);
551
+ }
552
+ }
553
+ } catch (e) {}
554
+ }
555
+ walkDir(scanDir);
556
+ }
557
+ filesToScan.push(...found);
558
+ }
559
+
560
+ if (filesToScan.length === 0) {
561
+ diag(`${c.yellow}No MCP configuration files found.${c.reset}`);
562
+ diag(`${c.dim}Searched paths: ${KNOWN_CONFIG_PATHS.join(', ')}${c.reset}`);
563
+ diag(`\n${c.dim}Config mode: correctover-scan path/to/mcp.json${c.reset}`);
564
+ const cwdHasPkg = fs.existsSync(path.join(scanDir, 'package.json'));
565
+ if (cwdHasPkg) {
566
+ diag(`${c.cyan}Detected package.json here — audit published/bundled code with: correctover-scan --bundle ${scanDir}${c.reset}`);
567
+ } else {
568
+ diag(`${c.dim}Audit an npm package / JS bundle instead: correctover-scan --bundle <path-to-package-or-js>${c.reset}`);
569
+ }
570
+ process.exit(0);
571
+ }
572
+
573
+ diag(`${c.dim}Found ${filesToScan.length} config file(s)${c.reset}\n`);
574
+
575
+ let totalPass = 0, totalWarn = 0, totalFail = 0, totalInfo = 0;
576
+ let allResults = [];
577
+
578
+ for (const fp of filesToScan) {
579
+ try {
580
+ const content = fs.readFileSync(fp, 'utf-8');
581
+ const config = parseConfig(content, fp);
582
+ const { results, stats } = runScan(config);
583
+ const relPath = path.relative(process.cwd(), fp) || fp;
584
+
585
+ if (format === 'text') {
586
+ emit(formatResults(results, stats, relPath));
587
+ emit('');
588
+ }
589
+
590
+ totalPass += stats.pass;
591
+ totalWarn += stats.warn;
592
+ totalFail += stats.fail;
593
+ totalInfo += stats.info;
594
+ allResults.push({ file: relPath, results, stats });
595
+ } catch (e) {
596
+ console.error(`${c.red}Error scanning ${fp}: ${e.message}${c.reset}\n`);
597
+ }
598
+ }
599
+
600
+ // JSON/SARIF/HTML output
601
+ let machinePayload = null;
602
+ let htmlDefault = false;
603
+ if (format === 'json') {
604
+ const output = allResults.map(r => formatJSON(r.results, r.stats, r.file));
605
+ machinePayload = output.join('\n');
606
+ } else if (format === 'sarif') {
607
+ const sarifResults = allResults.map(r => formatSARIF(r.results, r.file));
608
+ machinePayload = JSON.stringify(sarifResults.length === 1 ? sarifResults[0] : { runs: sarifResults.flatMap(s => s.runs) }, null, 2);
609
+ } else if (format === 'html') {
610
+ const totals = { pass: totalPass, warn: totalWarn, fail: totalFail, info: totalInfo };
611
+ const denom = (totalPass + totalWarn + totalFail + totalInfo) * 10;
612
+ totals.score = denom === 0 ? 100 : Math.round(((totalPass * 10 + totalWarn * 5 + totalInfo * 7) / denom) * 100);
613
+ machinePayload = formatHTMLReport({
614
+ mode: 'MCP config scan',
615
+ target: filesToScan.length === 1 ? path.relative(process.cwd(), filesToScan[0]) || filesToScan[0] : `${filesToScan.length} config file(s)`,
616
+ items: normalizeConfigFindings(allResults), totals,
617
+ checksLabel: '14 config checks mapped to OWASP AISVS 1.0',
618
+ version: VERSION,
619
+ });
620
+ if (!outFile) { outFile = defaultReportPath(); htmlDefault = true; }
621
+ }
622
+
623
+ // Summary
624
+ if (filesToScan.length > 1 && format === 'text') {
625
+ const totalScore = Math.round(((totalPass * 10 + totalWarn * 5 + totalInfo * 7) / ((totalPass + totalWarn + totalFail + totalInfo) * 10)) * 100);
626
+ emit(`${c.bold}═══ Summary ═══${c.reset}`);
627
+ emit(` Files scanned: ${filesToScan.length}`);
628
+ emit(` Total score: ${totalScore}/100`);
629
+ emit(` ${c.green}✓ ${totalPass}${c.reset} ${c.yellow}⚠ ${totalWarn}${c.reset} ${c.red}✗ ${totalFail}${c.reset} ${c.blue}ℹ ${totalInfo}${c.reset}`);
630
+ }
631
+
632
+ // -o: write the report payload to a file; otherwise stream to stdout
633
+ if (outFile) {
634
+ const payload = machinePayload || textBuffer.join('\n');
635
+ try {
636
+ fs.writeFileSync(outFile, payload + '\n');
637
+ diag(`${c.green}✅ Report written to ${outFile}${c.reset}`);
638
+ if (htmlDefault) {
639
+ diag(`${c.dim}Shareable HTML report — forward it to your team. ${failHint(totalFail)}${c.reset}`);
640
+ }
641
+ } catch (e) {
642
+ console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
643
+ process.exit(1);
644
+ }
645
+ } else if (machinePayload) {
646
+ console.log(machinePayload);
647
+ }
648
+
649
+ // Exit code: 1 if any critical failures
650
+ if (totalFail > 0) process.exit(1);
651
+ }
652
+
653
+ main();