correctover-scan 1.4.0 → 1.6.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/core/license.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * License validator for correctover-scan NPM.
3
- * Mirrors the Python LicenseValidator: 30 free checks/month (no daily reset), CORRECTOVER_LICENSE_KEY unlocks.
3
+ * Mirrors the Python LicenseValidator: 50 free scans/day, CORRECTOVER_LICENSE_KEY unlocks.
4
4
  * Supports CV-TRL/CV-PRO (FC) and COV- (Cloud) key formats.
5
5
  */
6
6
 
@@ -9,7 +9,7 @@ const path = require('path');
9
9
  const crypto = require('crypto');
10
10
  const os = require('os');
11
11
 
12
- const FREE_LIMIT_PER_MONTH = 30;
12
+ const FREE_LIMIT_PER_DAY = 50;
13
13
  const STATE_FILE = path.join(os.homedir(), '.correctover', 'license.json');
14
14
 
15
15
  function loadState() {
@@ -28,18 +28,14 @@ function saveState(state) {
28
28
  } catch (e) {}
29
29
  }
30
30
 
31
- function currentMonth() {
32
- return new Date().toISOString().slice(0, 7); // YYYY-MM
33
- }
34
-
35
31
  function getProductState(state, product) {
36
- const month = currentMonth();
32
+ const today = new Date().toISOString().slice(0, 10);
37
33
  if (!state.products[product]) {
38
- state.products[product] = { checks_used: 0, month: month, total_checks: 0 };
34
+ state.products[product] = { calls_today: 0, date: today, total_calls: 0 };
39
35
  }
40
- if (state.products[product].month !== month) {
41
- state.products[product].checks_used = 0;
42
- state.products[product].month = month;
36
+ if (state.products[product].date !== today) {
37
+ state.products[product].calls_today = 0;
38
+ state.products[product].date = today;
43
39
  }
44
40
  return state.products[product];
45
41
  }
@@ -84,59 +80,49 @@ function checkLicense(product) {
84
80
  return {
85
81
  authorized: true,
86
82
  tier: 'pro',
87
- checks_remaining: Infinity,
88
- checks_used: ps.checks_used,
83
+ calls_remaining: Infinity,
84
+ calls_today: ps.calls_today,
89
85
  limit: Infinity,
90
86
  };
91
87
  }
92
88
 
93
- const remaining = Math.max(0, FREE_LIMIT_PER_MONTH - ps.checks_used);
89
+ const remaining = Math.max(0, FREE_LIMIT_PER_DAY - ps.calls_today);
94
90
  return {
95
91
  authorized: remaining > 0,
96
92
  tier: 'free',
97
- checks_remaining: remaining,
98
- checks_used: ps.checks_used,
99
- limit: FREE_LIMIT_PER_MONTH,
93
+ calls_remaining: remaining,
94
+ calls_today: ps.calls_today,
95
+ limit: FREE_LIMIT_PER_DAY,
100
96
  };
101
97
  }
102
98
 
103
- function recordCall(product, count = 1) {
99
+ function recordCall(product) {
104
100
  const state = loadState();
105
101
  const status = checkLicense(product);
106
102
  if (!status.authorized) return status;
107
103
 
108
104
  const ps = getProductState(state, product);
109
- ps.checks_used += count;
110
- ps.total_checks = (ps.total_checks || 0) + count;
105
+ ps.calls_today += 1;
106
+ ps.total_calls = (ps.total_calls || 0) + 1;
111
107
  saveState(state);
112
108
 
113
- status.checks_remaining = Math.max(0, FREE_LIMIT_PER_MONTH - ps.checks_used);
114
- status.checks_used = ps.checks_used;
109
+ status.calls_remaining = Math.max(0, status.limit - ps.calls_today);
110
+ status.calls_today = ps.calls_today;
115
111
  return status;
116
112
  }
117
113
 
118
- function canRun(product, count) {
119
- const status = checkLicense(product);
120
- if (status.tier === 'pro') return true;
121
- return status.checks_remaining >= count;
122
- }
123
-
124
- function getUpgradeMessage(status, context) {
114
+ function getUpgradeMessage(status) {
125
115
  if (status.tier !== 'free') return '';
126
- if (status.checks_remaining <= 0) {
127
- return `\n🚫 Free tier limit reached (${FREE_LIMIT_PER_MONTH} checks/month).\n Upgrade to Pro: https://correctover.com/checkout\n Or: export CORRECTOVER_LICENSE_KEY=<your-key>\n`;
128
- }
129
- if (context === 'results') {
130
- return `\n${'━'.repeat(50)}\nšŸ”’ ${status.checks_remaining} checks remaining this month.\n Upgrade to Pro for:\n • Full risk report (all findings)\n • Fix recommendations + auto-heal\n • HTML audit reports\n${'━'.repeat(50)}\n → https://correctover.com/checkout\n${'━'.repeat(50)}\n`;
116
+ if (status.calls_remaining <= 0) {
117
+ return `\n🚫 Free tier limit reached (${FREE_LIMIT_PER_DAY} scans/day).\n Upgrade: https://correctover.com/checkout\n Or: export CORRECTOVER_LICENSE_KEY=<your-key>\n`;
131
118
  }
132
- return `\nšŸ“Š Free tier: ${status.checks_remaining} checks remaining this month.\n Upgrade: https://correctover.com/checkout\n`;
119
+ return `\nšŸ“Š Free tier: ${status.calls_remaining} scans remaining today.\n Upgrade: https://correctover.com/checkout\n`;
133
120
  }
134
121
 
135
122
  module.exports = {
136
- FREE_LIMIT_PER_MONTH,
123
+ FREE_LIMIT_PER_DAY,
137
124
  checkLicense,
138
125
  recordCall,
139
- canRun,
140
126
  getUpgradeMessage,
141
127
  verifyLicenseKey,
142
128
  };
package/core/scanner.js CHANGED
File without changes
package/index.js CHANGED
@@ -10,10 +10,12 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const { runScan, parseConfig, KNOWN_CONFIG_PATHS } = require('./core/scanner');
13
+ const { runBundleScan, discoverBundleFiles } = require('./core/bundle-scanner');
13
14
  const { recordCall, getUpgradeMessage } = require('./core/license');
14
15
 
15
- const VERSION = '1.2.0';
16
+ const VERSION = '1.6.0';
16
17
  const PRODUCT = 'correctover-scan';
18
+ const JS_EXT = new Set(['.js', '.mjs', '.cjs', '.ts']);
17
19
 
18
20
  // Colors
19
21
  const c = {
@@ -27,13 +29,14 @@ const icons = { pass: 'āœ…', warn: 'āš ļø', fail: 'āŒ', info: 'ā„¹ļø' };
27
29
  const sevColors = { critical: c.red, high: c.yellow, medium: c.cyan, low: c.gray };
28
30
  const sevLabels = { critical: 'CRITICAL', high: 'HIGH', medium: 'MEDIUM', low: 'LOW' };
29
31
 
30
- function printBanner() {
31
- console.log(`
32
+ function printBanner(stream) {
33
+ const out = stream === 'stderr' ? console.error : console.log;
34
+ out(`
32
35
  ${c.bold}${c.blue} ╔══════════════════════════════════════════╗
33
36
  ā•‘ CCS Security Scanner by Correctover ā•‘
34
37
  ā•‘ AI Agent Runtime Assurance ā•‘
35
38
  ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•${c.reset}
36
- ${c.dim}v${VERSION} | OWASP AISVS 1.0 | 14 security checks${c.reset}
39
+ ${c.dim}v${VERSION} | OWASP AISVS 1.0 | 14 checks Ā· MCP config + JS bundle modes${c.reset}
37
40
  `);
38
41
  }
39
42
 
@@ -122,11 +125,139 @@ function formatResults(results, stats, filename) {
122
125
  }
123
126
  lines.push(`${c.dim}Enterprise: runtime SDK + Token guarantee → https://correctover.com${c.reset}`);
124
127
  lines.push('');
125
- lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/Correctover${c.reset}`);
128
+ lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/DSHCorrectover${c.reset}`);
126
129
 
130
+ // Manual audit CTA
131
+ lines.push('');
132
+ 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}`);
133
+
134
+ return lines.join('\n');
135
+ }
136
+
137
+ /* ------------------------------------------------------------------ */
138
+ /* Bundle mode output */
139
+ /* ------------------------------------------------------------------ */
140
+
141
+ function formatBundleResults(bundle, baseLabel) {
142
+ const lines = [];
143
+ const stats = bundle.stats;
144
+ lines.push(`${c.bold}šŸ“¦ Bundle/code scan: ${baseLabel}${c.reset}`);
145
+ 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}`);
146
+ lines.push('─'.repeat(50));
147
+
148
+ const scoreColor = stats.score >= 80 ? c.green : stats.score >= 60 ? c.yellow : c.red;
149
+ lines.push(`\n${c.bold}Security Score: ${scoreColor}${c.bold}${stats.score}/100${c.reset}`);
150
+ 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}`);
151
+ 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`);
152
+
153
+ for (const fr of bundle.files) {
154
+ const active = [];
155
+ const suppressedCount = fr.results.reduce((n, r) => n + r.findings.filter(f => f.suppressed).length, 0);
156
+ for (const r of fr.results) {
157
+ for (const f of r.findings) {
158
+ if (!f.suppressed) active.push({ check: r, f });
159
+ }
160
+ }
161
+ if (active.length === 0) {
162
+ lines.push(`${c.green}āœ… ${fr.file} — no fail/warn findings (${suppressedCount} known-benign signals suppressed, see JSON for detail)${c.reset}\n`);
163
+ continue;
164
+ }
165
+ lines.push(`${c.bold}šŸ“„ ${fr.file}${c.reset}`);
166
+ for (const { check, f } of active) {
167
+ const icon = icons[f.severity === 'fail' ? 'fail' : f.severity === 'warn' ? 'warn' : 'info'];
168
+ const statusStr = f.severity === 'fail' ? c.red + 'FAIL' : f.severity === 'warn' ? c.yellow + 'WARN' : c.blue + 'INFO';
169
+ lines.push(` ${icon} ${c.dim}[${check.id} Ā· ${check.aisvs}]${c.reset} ${statusStr}${c.reset} ${c.bold}L${f.line}${c.reset}`);
170
+ lines.push(` ${f.message}`);
171
+ if (f.snippet) lines.push(` ${c.gray}${f.snippet}${c.reset}`);
172
+ lines.push('');
173
+ }
174
+ }
175
+
176
+ // Semi-automatic checks summary
177
+ const semiIds = ['budget-limit', 'logging', 'version-pin', 'error-handling', 'output-validation'];
178
+ lines.push(`${c.dim}── Semi-automatic checks (signals enumerated; conclusion needs manual review) ──${c.reset}`);
179
+ for (const id of semiIds) {
180
+ let n = 0;
181
+ for (const fr of bundle.files) {
182
+ const r = fr.results.find(x => x.id === id);
183
+ if (r) n += r.findings.length;
184
+ }
185
+ lines.push(` ${c.blue}ℹ${c.reset} ${c.dim}${id}: ${n} signal(s) listed in JSON output${c.reset}`);
186
+ }
187
+
188
+ lines.push('');
189
+ lines.push(`${c.dim}Note: bundle mode is signal scanning over published/minified code. It locates every${c.reset}`);
190
+ lines.push(`${c.dim}risk-bearing string/API call with file+line; it does not prove reachability or intent —${c.reset}`);
191
+ lines.push(`${c.dim}fail/warn items and the semi-automatic signals are the input for manual deep review.${c.reset}`);
192
+
193
+ lines.push('');
194
+ lines.push(`${c.dim}── Upgrade ─────────────────────────────────${c.reset}`);
195
+ if (stats.fail > 0 || stats.score < 60) {
196
+ lines.push(`${c.yellow}${c.bold}⚔ Critical findings present. CCS Pro generates formal compliance reports.${c.reset}`);
197
+ lines.push(`${c.dim} Enterprise: real-time runtime protection + Token guarantee${c.reset}`);
198
+ lines.push(`${c.cyan} → Upgrade: https://correctover.com/checkout${c.reset}`);
199
+ } else {
200
+ lines.push(`${c.green}āœ“ No critical signals! Get a formal compliance certificate with CCS Pro.${c.reset}`);
201
+ lines.push(`${c.cyan} → https://correctover.com/checkout${c.reset}`);
202
+ }
203
+ lines.push(`${c.dim}Manual bundle audit (116-rule deep review): https://dshcorrectover.github.io/agent-audit/${c.reset}`);
204
+ lines.push('');
205
+ lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/DSHCorrectover${c.reset}`);
127
206
  return lines.join('\n');
128
207
  }
129
208
 
209
+ function formatBundleJSON(bundle, target) {
210
+ return JSON.stringify({
211
+ scanner: 'correctover-scan', version: VERSION, mode: 'bundle', target,
212
+ stats: bundle.stats,
213
+ files: bundle.files.map(fr => ({
214
+ file: fr.file,
215
+ results: fr.results.map(r => ({
216
+ id: r.id, name: r.name, category: r.category, aisvs: r.aisvs,
217
+ severity: r.severity, status: r.status, fix: r.fix,
218
+ findings: r.findings.map(f => ({ line: f.line, severity: f.severity, suppressed: f.suppressed, message: f.message, snippet: f.snippet })),
219
+ })),
220
+ })),
221
+ }, null, 2);
222
+ }
223
+
224
+ function formatBundleSARIF(bundle, target) {
225
+ const rules = [];
226
+ const results = [];
227
+ for (const fr of bundle.files) {
228
+ for (const r of fr.results) {
229
+ if (!rules.some(x => x.id === r.id)) {
230
+ rules.push({
231
+ id: r.id, name: r.name,
232
+ shortDescription: { text: `${r.category}: ${r.name}` },
233
+ helpUri: `https://correctover.com/scan/#check-${r.id}`,
234
+ properties: { aisvs: r.aisvs, severity: r.severity, mode: 'bundle' },
235
+ });
236
+ }
237
+ for (const f of r.findings) {
238
+ if (f.suppressed || f.severity === 'info') continue;
239
+ results.push({
240
+ ruleId: r.id,
241
+ level: f.severity === 'fail' ? 'error' : 'warning',
242
+ message: { text: f.message },
243
+ locations: [{ physicalLocation: {
244
+ artifactLocation: { uri: fr.file },
245
+ region: f.line ? { startLine: f.line } : undefined,
246
+ } }],
247
+ });
248
+ }
249
+ }
250
+ }
251
+ return {
252
+ version: '2.1.0',
253
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
254
+ runs: [{
255
+ tool: { driver: { name: 'correctover-scan', version: VERSION, informationUri: 'https://correctover.com', rules } },
256
+ results,
257
+ }],
258
+ };
259
+ }
260
+
130
261
  function formatSARIF(results, filename) {
131
262
  return {
132
263
  version: '2.1.0',
@@ -160,12 +291,81 @@ function formatJSON(results, stats, filename) {
160
291
  return JSON.stringify({ scanner: 'correctover-scan', version: VERSION, file: filename, stats, results }, null, 2);
161
292
  }
162
293
 
294
+ function runBundleMode(target, format, outFile) {
295
+ // All diagnostics/banners go to stderr so that stdout carries only the
296
+ // machine-readable payload for json/sarif consumers.
297
+ const diag = (...a) => console.error(...a);
298
+ printBanner(format === 'text' && !outFile ? 'stdout' : 'stderr');
299
+
300
+ // License check (same 50/day free tier counting as config mode)
301
+ const status = recordCall(PRODUCT);
302
+ if (!status.authorized) {
303
+ diag(getUpgradeMessage(status));
304
+ process.exit(1);
305
+ }
306
+ if (status.tier === 'free') {
307
+ diag(`${c.dim}šŸ“Š Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
308
+ diag(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}\n`);
309
+ } else if (status.tier === 'pro') {
310
+ diag(`${c.green}āœ… Pro license active — unlimited scans${c.reset}\n`);
311
+ }
312
+
313
+ if (!fs.existsSync(target)) {
314
+ diag(`${c.red}Error: path not found: ${target}${c.reset}`);
315
+ process.exit(1);
316
+ }
317
+
318
+ const abs = path.resolve(target);
319
+ const isDir = fs.statSync(abs).isDirectory();
320
+ const baseDir = isDir ? abs : path.dirname(abs);
321
+ let files;
322
+ try {
323
+ files = discoverBundleFiles(abs);
324
+ } catch (e) {
325
+ diag(`${c.red}Error discovering bundle files: ${e.message}${c.reset}`);
326
+ process.exit(1);
327
+ }
328
+ if (files.length === 0) {
329
+ diag(`${c.yellow}No JavaScript files (.js/.mjs/.cjs) found under ${target}.${c.reset}`);
330
+ process.exit(0);
331
+ }
332
+ diag(`${c.dim}Bundle mode: ${files.length} JS file(s) under ${target}${c.reset}\n`);
333
+
334
+ const bundle = runBundleScan(files, baseDir);
335
+ const label = path.relative(process.cwd(), abs) || abs;
336
+
337
+ let payload;
338
+ if (format === 'json') {
339
+ payload = formatBundleJSON(bundle, label);
340
+ } else if (format === 'sarif') {
341
+ payload = JSON.stringify(formatBundleSARIF(bundle, label), null, 2);
342
+ } else {
343
+ payload = formatBundleResults(bundle, label);
344
+ }
345
+ if (outFile) {
346
+ try {
347
+ fs.writeFileSync(outFile, payload + '\n');
348
+ diag(`${c.green}āœ… Report written to ${outFile}${c.reset}`);
349
+ } catch (e) {
350
+ console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
351
+ process.exit(1);
352
+ }
353
+ } else {
354
+ console.log(payload);
355
+ }
356
+
357
+ // Exit code: 1 if any check has fail-level findings
358
+ if (bundle.stats.findings.fail > 0) process.exit(1);
359
+ }
360
+
163
361
  function main() {
164
362
  const args = process.argv.slice(2);
165
363
  let configPath = null;
364
+ let bundlePath = null;
166
365
  let format = 'text'; // text, json, sarif
167
366
  let scanDir = process.cwd();
168
367
  let recursive = false;
368
+ let outFile = null;
169
369
 
170
370
  // Parse args
171
371
  for (let i = 0; i < args.length; i++) {
@@ -175,20 +375,38 @@ function main() {
175
375
  process.exit(0);
176
376
  } else if (arg === '--help' || arg === '-h') {
177
377
  console.log(`
178
- Usage: correctover-scan [config-file] [options]
378
+ Usage: correctover-scan [target] [options]
379
+
380
+ MCP config mode (default for .json/.yaml/mcp.json):
381
+ correctover-scan mcp.json Scan a specific MCP config
382
+ correctover-scan -d ./project Auto-detect configs in directory
383
+ correctover-scan -d ./project -r Recursively find config files
384
+
385
+ Bundle / published-package code mode (v1.4.0+):
386
+ correctover-scan --bundle ./pkg Audit an unpacked npm package
387
+ (reads package.json entry + all .js/.mjs/.cjs)
388
+ correctover-scan --bundle dist/app.min.js Audit a single minified/bundled JS file
389
+ correctover-scan ./cli.js Auto-detected: .js/.mjs/.cjs target → bundle mode
390
+ correctover-scan --bundle ./pkg -f sarif SARIF with file+line locations
179
391
 
180
392
  Options:
393
+ --bundle <path> Explicitly run bundle/code scan on a JS file or directory
181
394
  -f, --format <type> Output format: text, json, sarif (default: text)
182
395
  -d, --dir <path> Directory to scan for MCP configs (default: cwd)
183
396
  -r, --recursive Recursively find MCP config files
397
+ -o, --output <file> Write the report to a file (text/json/sarif) instead of stdout
184
398
  -v, --version Show version
185
399
  -h, --help Show this help
186
400
 
401
+ Bundle mode performs signal scanning of shipped/minified code (hardcoded secrets,
402
+ plaintext endpoints, cloud metadata/SSRF, shell:true/exec, eval/Function/vm, env
403
+ credential flow, permission gates, MCP transport auth, timeouts, kill switch,
404
+ sandbox, input validation) with context-based false-positive suppression. It
405
+ locates signals at file+line; reachability/intent conclusions need manual review.
406
+
187
407
  Examples:
188
- correctover-scan mcp.json Scan a specific file
189
- correctover-scan -d ./project Auto-detect configs in directory
190
- correctover-scan -f sarif -o report.sarif Output SARIF format
191
- npx correctover-scan Auto-detect in current directory
408
+ npx correctover-scan Auto-detect MCP configs in cwd
409
+ npx correctover-scan --bundle . Audit the current package's code
192
410
  `);
193
411
  process.exit(0);
194
412
  } else if (arg === '--format' || arg === '-f') {
@@ -197,26 +415,60 @@ Examples:
197
415
  scanDir = args[++i];
198
416
  } else if (arg === '--recursive' || arg === '-r') {
199
417
  recursive = true;
418
+ } else if (arg === '--bundle' || arg === '-b') {
419
+ bundlePath = args[++i];
200
420
  } else if (arg === '--output' || arg === '-o') {
201
- // Output file - handled below
421
+ outFile = args[++i] || null;
202
422
  } else if (!arg.startsWith('-')) {
203
423
  configPath = arg;
204
424
  }
205
425
  }
206
426
 
207
- printBanner();
427
+ // Auto-detect bundle mode: positional .js/.mjs/.cjs file, or a directory
428
+ // that contains JS but no MCP config and looks like a package.
429
+ if (!bundlePath && configPath && fs.existsSync(configPath)) {
430
+ const st = fs.statSync(configPath);
431
+ if (st.isFile() && JS_EXT.has(path.extname(configPath).toLowerCase())) {
432
+ bundlePath = configPath;
433
+ configPath = null;
434
+ } else if (st.isDirectory() && !configPath.endsWith('.json')) {
435
+ // directory positional: prefer config mode only if a known config exists
436
+ const hasConfig = KNOWN_CONFIG_PATHS.some(rel => fs.existsSync(path.join(configPath, rel)));
437
+ if (!hasConfig) {
438
+ let hasJS = false;
439
+ try { hasJS = fs.readdirSync(configPath).some(f => JS_EXT.has(path.extname(f).toLowerCase())); } catch (e) {}
440
+ if (hasJS || fs.existsSync(path.join(configPath, 'package.json'))) {
441
+ bundlePath = configPath;
442
+ configPath = null;
443
+ }
444
+ }
445
+ }
446
+ }
447
+
448
+ if (bundlePath) {
449
+ return runBundleMode(bundlePath, format, outFile);
450
+ }
451
+
452
+ // In json/sarif mode all diagnostics go to stderr, keeping stdout clean.
453
+ const diag = (format === 'text' && !outFile) ? console.log : (...a) => console.error(...a);
454
+ printBanner(format === 'text' && !outFile ? 'stdout' : 'stderr');
455
+
456
+ // Text-mode payload buffer: when -o writes a report file, the report text is
457
+ // collected instead of being streamed to stdout (diagnostics stay on stderr).
458
+ const textBuffer = outFile ? [] : null;
459
+ const emit = textBuffer ? (s) => textBuffer.push(s) : (s) => console.log(s);
208
460
 
209
461
  // License check
210
462
  const status = recordCall(PRODUCT);
211
463
  if (!status.authorized) {
212
- console.log(getUpgradeMessage(status));
464
+ diag(getUpgradeMessage(status));
213
465
  process.exit(1);
214
466
  }
215
467
  if (status.tier === 'free') {
216
- console.log(`${c.dim}šŸ“Š Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
217
- console.log(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}\n`);
468
+ diag(`${c.dim}šŸ“Š Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
469
+ diag(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}\n`);
218
470
  } else if (status.tier === 'pro') {
219
- console.log(`${c.green}āœ… Pro license active — unlimited scans${c.reset}\n`);
471
+ diag(`${c.green}āœ… Pro license active — unlimited scans${c.reset}\n`);
220
472
  }
221
473
 
222
474
  const filesToScan = [];
@@ -230,7 +482,7 @@ Examples:
230
482
  filesToScan.push(configPath);
231
483
  } else {
232
484
  // Auto-detect
233
- console.log(`${c.dim}Auto-detecting MCP configs in ${scanDir}...${c.reset}\n`);
485
+ diag(`${c.dim}Auto-detecting MCP configs in ${scanDir}...${c.reset}\n`);
234
486
  const found = findConfigFiles(scanDir);
235
487
  if (recursive) {
236
488
  // Walk directory tree
@@ -254,13 +506,19 @@ Examples:
254
506
  }
255
507
 
256
508
  if (filesToScan.length === 0) {
257
- console.log(`${c.yellow}No MCP configuration files found.${c.reset}`);
258
- console.log(`${c.dim}Searched paths: ${KNOWN_CONFIG_PATHS.join(', ')}${c.reset}`);
259
- console.log(`\n${c.dim}Create a config file or specify one: correctover-scan path/to/mcp.json${c.reset}`);
509
+ diag(`${c.yellow}No MCP configuration files found.${c.reset}`);
510
+ diag(`${c.dim}Searched paths: ${KNOWN_CONFIG_PATHS.join(', ')}${c.reset}`);
511
+ diag(`\n${c.dim}Config mode: correctover-scan path/to/mcp.json${c.reset}`);
512
+ const cwdHasPkg = fs.existsSync(path.join(scanDir, 'package.json'));
513
+ if (cwdHasPkg) {
514
+ diag(`${c.cyan}Detected package.json here — audit published/bundled code with: correctover-scan --bundle ${scanDir}${c.reset}`);
515
+ } else {
516
+ diag(`${c.dim}Audit an npm package / JS bundle instead: correctover-scan --bundle <path-to-package-or-js>${c.reset}`);
517
+ }
260
518
  process.exit(0);
261
519
  }
262
520
 
263
- console.log(`${c.dim}Found ${filesToScan.length} config file(s)${c.reset}\n`);
521
+ diag(`${c.dim}Found ${filesToScan.length} config file(s)${c.reset}\n`);
264
522
 
265
523
  let totalPass = 0, totalWarn = 0, totalFail = 0, totalInfo = 0;
266
524
  let allResults = [];
@@ -273,8 +531,8 @@ Examples:
273
531
  const relPath = path.relative(process.cwd(), fp) || fp;
274
532
 
275
533
  if (format === 'text') {
276
- console.log(formatResults(results, stats, relPath));
277
- console.log('');
534
+ emit(formatResults(results, stats, relPath));
535
+ emit('');
278
536
  }
279
537
 
280
538
  totalPass += stats.pass;
@@ -288,21 +546,36 @@ Examples:
288
546
  }
289
547
 
290
548
  // JSON/SARIF output
549
+ let machinePayload = null;
291
550
  if (format === 'json') {
292
551
  const output = allResults.map(r => formatJSON(r.results, r.stats, r.file));
293
- console.log(output.join('\n'));
552
+ machinePayload = output.join('\n');
294
553
  } else if (format === 'sarif') {
295
554
  const sarifResults = allResults.map(r => formatSARIF(r.results, r.file));
296
- console.log(JSON.stringify(sarifResults.length === 1 ? sarifResults[0] : { runs: sarifResults.flatMap(s => s.runs) }, null, 2));
555
+ machinePayload = JSON.stringify(sarifResults.length === 1 ? sarifResults[0] : { runs: sarifResults.flatMap(s => s.runs) }, null, 2);
297
556
  }
298
557
 
299
558
  // Summary
300
559
  if (filesToScan.length > 1 && format === 'text') {
301
560
  const totalScore = Math.round(((totalPass * 10 + totalWarn * 5 + totalInfo * 7) / ((totalPass + totalWarn + totalFail + totalInfo) * 10)) * 100);
302
- console.log(`${c.bold}═══ Summary ═══${c.reset}`);
303
- console.log(` Files scanned: ${filesToScan.length}`);
304
- console.log(` Total score: ${totalScore}/100`);
305
- console.log(` ${c.green}āœ“ ${totalPass}${c.reset} ${c.yellow}⚠ ${totalWarn}${c.reset} ${c.red}āœ— ${totalFail}${c.reset} ${c.blue}ℹ ${totalInfo}${c.reset}`);
561
+ emit(`${c.bold}═══ Summary ═══${c.reset}`);
562
+ emit(` Files scanned: ${filesToScan.length}`);
563
+ emit(` Total score: ${totalScore}/100`);
564
+ emit(` ${c.green}āœ“ ${totalPass}${c.reset} ${c.yellow}⚠ ${totalWarn}${c.reset} ${c.red}āœ— ${totalFail}${c.reset} ${c.blue}ℹ ${totalInfo}${c.reset}`);
565
+ }
566
+
567
+ // -o: write the report payload to a file; otherwise stream to stdout
568
+ if (outFile) {
569
+ const payload = machinePayload || textBuffer.join('\n');
570
+ try {
571
+ fs.writeFileSync(outFile, payload + '\n');
572
+ diag(`${c.green}āœ… Report written to ${outFile}${c.reset}`);
573
+ } catch (e) {
574
+ console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
575
+ process.exit(1);
576
+ }
577
+ } else if (machinePayload) {
578
+ console.log(machinePayload);
306
579
  }
307
580
 
308
581
  // Exit code: 1 if any critical failures
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "correctover-scan",
3
- "version": "1.4.0",
4
- "description": "MCP security scanner — 1 command, 14 OWASP AISVS 1.0 checks. Detect RCE, SSRF, credential exposure. Free: 30 checks/month.",
3
+ "version": "1.6.0",
4
+ "description": "Correctover Security Scanner — CCS audit for MCP configurations AND published JS bundles/npm packages. Detects hardcoded secrets, RCE, SSRF, credential hijacking against OWASP AISVS 1.0.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "correctover-scan": "./index.js"
@@ -12,33 +12,51 @@
12
12
  },
13
13
  "keywords": [
14
14
  "mcp",
15
+ "mcp-security",
16
+ "agent-security",
17
+ "ai-security",
18
+ "runtime-security",
15
19
  "security",
16
20
  "scanner",
17
- "agent",
18
- "ai",
19
21
  "owasp",
20
22
  "aisvs",
23
+ "owasp-aisvs",
21
24
  "ccs",
22
25
  "correctover",
26
+ "vulnerability-scanner",
27
+ "rce-detection",
28
+ "ssrf-detection",
29
+ "credential-hijacking",
30
+ "self-healing",
31
+ "audit",
32
+ "guardrail",
23
33
  "claude",
24
34
  "cursor",
25
35
  "model-context-protocol",
26
- "vulnerability",
27
- "audit"
36
+ "security-scanner",
37
+ "ai-agents",
38
+ "llm",
39
+ "llm-security",
40
+ "devsecops",
41
+ "cli",
42
+ "prompt-injection",
43
+ "command-injection",
44
+ "ssrf"
28
45
  ],
29
46
  "author": "Correctover",
30
47
  "license": "MIT",
31
48
  "repository": {
32
49
  "type": "git",
33
- "url": "https://github.com/Correctover/correctover-scan"
50
+ "url": "https://github.com/DSHCorrectover/correctover-scan"
34
51
  },
35
52
  "homepage": "https://correctover.com/scan",
36
53
  "bugs": {
37
- "url": "https://github.com/Correctover/correctover-scan/issues"
54
+ "url": "https://github.com/DSHCorrectover/correctover-scan/issues"
38
55
  },
39
56
  "files": [
40
57
  "index.js",
41
58
  "core/scanner.js",
59
+ "core/bundle-scanner.js",
42
60
  "core/license.js",
43
61
  "README.md"
44
62
  ],