correctover-scan 1.5.0 → 1.7.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
@@ -10,10 +10,13 @@
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');
15
+ const { formatHTMLReport, normalizeConfigFindings, normalizeBundleFindings, failHint } = require('./core/report');
14
16
 
15
- const VERSION = '1.2.0';
17
+ const VERSION = '1.7.0';
16
18
  const PRODUCT = 'correctover-scan';
19
+ const JS_EXT = new Set(['.js', '.mjs', '.cjs', '.ts']);
17
20
 
18
21
  // Colors
19
22
  const c = {
@@ -27,13 +30,14 @@ const icons = { pass: '✅', warn: '⚠️', fail: '❌', info: 'ℹ️' };
27
30
  const sevColors = { critical: c.red, high: c.yellow, medium: c.cyan, low: c.gray };
28
31
  const sevLabels = { critical: 'CRITICAL', high: 'HIGH', medium: 'MEDIUM', low: 'LOW' };
29
32
 
30
- function printBanner() {
31
- console.log(`
33
+ function printBanner(stream) {
34
+ const out = stream === 'stderr' ? console.error : console.log;
35
+ out(`
32
36
  ${c.bold}${c.blue} ╔══════════════════════════════════════════╗
33
37
  ║ CCS Security Scanner by Correctover ║
34
38
  ║ AI Agent Runtime Assurance ║
35
39
  ╚══════════════════════════════════════════╝${c.reset}
36
- ${c.dim}v${VERSION} | OWASP AISVS 1.0 | 14 security checks${c.reset}
40
+ ${c.dim}v${VERSION} | OWASP AISVS 1.0 | 14 checks · MCP config + JS bundle modes${c.reset}
37
41
  `);
38
42
  }
39
43
 
@@ -122,11 +126,139 @@ function formatResults(results, stats, filename) {
122
126
  }
123
127
  lines.push(`${c.dim}Enterprise: runtime SDK + Token guarantee → https://correctover.com${c.reset}`);
124
128
  lines.push('');
125
- lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/Correctover${c.reset}`);
129
+ lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/DSHCorrectover${c.reset}`);
126
130
 
131
+ // Manual audit CTA
132
+ lines.push('');
133
+ 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}`);
134
+
135
+ return lines.join('\n');
136
+ }
137
+
138
+ /* ------------------------------------------------------------------ */
139
+ /* Bundle mode output */
140
+ /* ------------------------------------------------------------------ */
141
+
142
+ function formatBundleResults(bundle, baseLabel) {
143
+ const lines = [];
144
+ const stats = bundle.stats;
145
+ lines.push(`${c.bold}📦 Bundle/code scan: ${baseLabel}${c.reset}`);
146
+ 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}`);
147
+ lines.push('─'.repeat(50));
148
+
149
+ const scoreColor = stats.score >= 80 ? c.green : stats.score >= 60 ? c.yellow : c.red;
150
+ lines.push(`\n${c.bold}Security Score: ${scoreColor}${c.bold}${stats.score}/100${c.reset}`);
151
+ 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}`);
152
+ 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`);
153
+
154
+ for (const fr of bundle.files) {
155
+ const active = [];
156
+ const suppressedCount = fr.results.reduce((n, r) => n + r.findings.filter(f => f.suppressed).length, 0);
157
+ for (const r of fr.results) {
158
+ for (const f of r.findings) {
159
+ if (!f.suppressed) active.push({ check: r, f });
160
+ }
161
+ }
162
+ if (active.length === 0) {
163
+ lines.push(`${c.green}✅ ${fr.file} — no fail/warn findings (${suppressedCount} known-benign signals suppressed, see JSON for detail)${c.reset}\n`);
164
+ continue;
165
+ }
166
+ lines.push(`${c.bold}📄 ${fr.file}${c.reset}`);
167
+ for (const { check, f } of active) {
168
+ const icon = icons[f.severity === 'fail' ? 'fail' : f.severity === 'warn' ? 'warn' : 'info'];
169
+ const statusStr = f.severity === 'fail' ? c.red + 'FAIL' : f.severity === 'warn' ? c.yellow + 'WARN' : c.blue + 'INFO';
170
+ lines.push(` ${icon} ${c.dim}[${check.id} · ${check.aisvs}]${c.reset} ${statusStr}${c.reset} ${c.bold}L${f.line}${c.reset}`);
171
+ lines.push(` ${f.message}`);
172
+ if (f.snippet) lines.push(` ${c.gray}${f.snippet}${c.reset}`);
173
+ lines.push('');
174
+ }
175
+ }
176
+
177
+ // Semi-automatic checks summary
178
+ const semiIds = ['budget-limit', 'logging', 'version-pin', 'error-handling', 'output-validation'];
179
+ lines.push(`${c.dim}── Semi-automatic checks (signals enumerated; conclusion needs manual review) ──${c.reset}`);
180
+ for (const id of semiIds) {
181
+ let n = 0;
182
+ for (const fr of bundle.files) {
183
+ const r = fr.results.find(x => x.id === id);
184
+ if (r) n += r.findings.length;
185
+ }
186
+ lines.push(` ${c.blue}ℹ${c.reset} ${c.dim}${id}: ${n} signal(s) listed in JSON output${c.reset}`);
187
+ }
188
+
189
+ lines.push('');
190
+ lines.push(`${c.dim}Note: bundle mode is signal scanning over published/minified code. It locates every${c.reset}`);
191
+ lines.push(`${c.dim}risk-bearing string/API call with file+line; it does not prove reachability or intent —${c.reset}`);
192
+ lines.push(`${c.dim}fail/warn items and the semi-automatic signals are the input for manual deep review.${c.reset}`);
193
+
194
+ lines.push('');
195
+ lines.push(`${c.dim}── Upgrade ─────────────────────────────────${c.reset}`);
196
+ if (stats.fail > 0 || stats.score < 60) {
197
+ lines.push(`${c.yellow}${c.bold}⚡ Critical findings present. CCS Pro generates formal compliance reports.${c.reset}`);
198
+ lines.push(`${c.dim} Enterprise: real-time runtime protection + Token guarantee${c.reset}`);
199
+ lines.push(`${c.cyan} → Upgrade: https://correctover.com/checkout${c.reset}`);
200
+ } else {
201
+ lines.push(`${c.green}✓ No critical signals! Get a formal compliance certificate with CCS Pro.${c.reset}`);
202
+ lines.push(`${c.cyan} → https://correctover.com/checkout${c.reset}`);
203
+ }
204
+ lines.push(`${c.dim}Manual bundle audit (116-rule deep review): https://dshcorrectover.github.io/agent-audit/${c.reset}`);
205
+ lines.push('');
206
+ lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/DSHCorrectover${c.reset}`);
127
207
  return lines.join('\n');
128
208
  }
129
209
 
210
+ function formatBundleJSON(bundle, target) {
211
+ return JSON.stringify({
212
+ scanner: 'correctover-scan', version: VERSION, mode: 'bundle', target,
213
+ stats: bundle.stats,
214
+ files: bundle.files.map(fr => ({
215
+ file: fr.file,
216
+ results: fr.results.map(r => ({
217
+ id: r.id, name: r.name, category: r.category, aisvs: r.aisvs,
218
+ severity: r.severity, status: r.status, fix: r.fix,
219
+ findings: r.findings.map(f => ({ line: f.line, severity: f.severity, suppressed: f.suppressed, message: f.message, snippet: f.snippet })),
220
+ })),
221
+ })),
222
+ }, null, 2);
223
+ }
224
+
225
+ function formatBundleSARIF(bundle, target) {
226
+ const rules = [];
227
+ const results = [];
228
+ for (const fr of bundle.files) {
229
+ for (const r of fr.results) {
230
+ if (!rules.some(x => x.id === r.id)) {
231
+ rules.push({
232
+ id: r.id, name: r.name,
233
+ shortDescription: { text: `${r.category}: ${r.name}` },
234
+ helpUri: `https://correctover.com/scan/#check-${r.id}`,
235
+ properties: { aisvs: r.aisvs, severity: r.severity, mode: 'bundle' },
236
+ });
237
+ }
238
+ for (const f of r.findings) {
239
+ if (f.suppressed || f.severity === 'info') continue;
240
+ results.push({
241
+ ruleId: r.id,
242
+ level: f.severity === 'fail' ? 'error' : 'warning',
243
+ message: { text: f.message },
244
+ locations: [{ physicalLocation: {
245
+ artifactLocation: { uri: fr.file },
246
+ region: f.line ? { startLine: f.line } : undefined,
247
+ } }],
248
+ });
249
+ }
250
+ }
251
+ }
252
+ return {
253
+ version: '2.1.0',
254
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
255
+ runs: [{
256
+ tool: { driver: { name: 'correctover-scan', version: VERSION, informationUri: 'https://correctover.com', rules } },
257
+ results,
258
+ }],
259
+ };
260
+ }
261
+
130
262
  function formatSARIF(results, filename) {
131
263
  return {
132
264
  version: '2.1.0',
@@ -160,12 +292,111 @@ function formatJSON(results, stats, filename) {
160
292
  return JSON.stringify({ scanner: 'correctover-scan', version: VERSION, file: filename, stats, results }, null, 2);
161
293
  }
162
294
 
295
+ /* ------------------------------------------------------------------ */
296
+ /* HTML shareable report (verification-as-acquisition funnel) */
297
+ /* ------------------------------------------------------------------ */
298
+
299
+ function defaultReportPath() {
300
+ const d = new Date();
301
+ const pad = (n) => String(n).padStart(2, '0');
302
+ const base = `correctover-scan-report-${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
303
+ let p = path.join(process.cwd(), `${base}.html`);
304
+ let n = 2;
305
+ while (fs.existsSync(p)) p = path.join(process.cwd(), `${base}-${n++}.html`);
306
+ return p;
307
+ }
308
+
309
+ function runBundleMode(target, format, outFile) {
310
+ // All diagnostics/banners go to stderr so that stdout carries only the
311
+ // machine-readable payload for json/sarif consumers.
312
+ const diag = (...a) => console.error(...a);
313
+ printBanner(format === 'text' && !outFile ? 'stdout' : 'stderr');
314
+
315
+ // License check (same 50/day free tier counting as config mode)
316
+ const status = recordCall(PRODUCT);
317
+ if (!status.authorized) {
318
+ diag(getUpgradeMessage(status));
319
+ process.exit(1);
320
+ }
321
+ if (status.tier === 'free') {
322
+ diag(`${c.dim}📊 Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
323
+ diag(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}\n`);
324
+ } else if (status.tier === 'pro') {
325
+ diag(`${c.green}✅ Pro license active — unlimited scans${c.reset}\n`);
326
+ }
327
+
328
+ if (!fs.existsSync(target)) {
329
+ diag(`${c.red}Error: path not found: ${target}${c.reset}`);
330
+ process.exit(1);
331
+ }
332
+
333
+ const abs = path.resolve(target);
334
+ const isDir = fs.statSync(abs).isDirectory();
335
+ const baseDir = isDir ? abs : path.dirname(abs);
336
+ let files;
337
+ try {
338
+ files = discoverBundleFiles(abs);
339
+ } catch (e) {
340
+ diag(`${c.red}Error discovering bundle files: ${e.message}${c.reset}`);
341
+ process.exit(1);
342
+ }
343
+ if (files.length === 0) {
344
+ diag(`${c.yellow}No JavaScript files (.js/.mjs/.cjs) found under ${target}.${c.reset}`);
345
+ process.exit(0);
346
+ }
347
+ diag(`${c.dim}Bundle mode: ${files.length} JS file(s) under ${target}${c.reset}\n`);
348
+
349
+ const bundle = runBundleScan(files, baseDir);
350
+ const label = path.relative(process.cwd(), abs) || abs;
351
+
352
+ let payload;
353
+ let htmlDefault = false;
354
+ if (format === 'json') {
355
+ payload = formatBundleJSON(bundle, label);
356
+ } else if (format === 'sarif') {
357
+ payload = JSON.stringify(formatBundleSARIF(bundle, label), null, 2);
358
+ } else if (format === 'html') {
359
+ const labelCounts = {
360
+ pass: bundle.stats.pass, warn: bundle.stats.warn, fail: bundle.stats.fail, info: bundle.stats.info,
361
+ score: bundle.stats.score,
362
+ };
363
+ payload = formatHTMLReport({
364
+ mode: 'bundle/code scan', target: label,
365
+ items: normalizeBundleFindings(bundle), totals: labelCounts,
366
+ checksLabel: '12 automatic + 5 semi-automatic code-layer checks',
367
+ version: VERSION,
368
+ });
369
+ if (!outFile) { outFile = defaultReportPath(); htmlDefault = true; }
370
+ } else {
371
+ payload = formatBundleResults(bundle, label);
372
+ }
373
+ if (outFile) {
374
+ try {
375
+ fs.writeFileSync(outFile, payload + '\n');
376
+ diag(`${c.green}✅ Report written to ${outFile}${c.reset}`);
377
+ if (htmlDefault) {
378
+ diag(`${c.dim}Shareable HTML report — forward it to your team. ${failHint(bundle.stats.findings.fail)}${c.reset}`);
379
+ }
380
+ } catch (e) {
381
+ console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
382
+ process.exit(1);
383
+ }
384
+ } else {
385
+ console.log(payload);
386
+ }
387
+
388
+ // Exit code: 1 if any check has fail-level findings
389
+ if (bundle.stats.findings.fail > 0) process.exit(1);
390
+ }
391
+
163
392
  function main() {
164
393
  const args = process.argv.slice(2);
165
394
  let configPath = null;
395
+ let bundlePath = null;
166
396
  let format = 'text'; // text, json, sarif
167
397
  let scanDir = process.cwd();
168
398
  let recursive = false;
399
+ let outFile = null;
169
400
 
170
401
  // Parse args
171
402
  for (let i = 0; i < args.length; i++) {
@@ -175,20 +406,40 @@ function main() {
175
406
  process.exit(0);
176
407
  } else if (arg === '--help' || arg === '-h') {
177
408
  console.log(`
178
- Usage: correctover-scan [config-file] [options]
409
+ Usage: correctover-scan [target] [options]
410
+
411
+ MCP config mode (default for .json/.yaml/mcp.json):
412
+ correctover-scan mcp.json Scan a specific MCP config
413
+ correctover-scan -d ./project Auto-detect configs in directory
414
+ correctover-scan -d ./project -r Recursively find config files
415
+
416
+ Bundle / published-package code mode (v1.4.0+):
417
+ correctover-scan --bundle ./pkg Audit an unpacked npm package
418
+ (reads package.json entry + all .js/.mjs/.cjs)
419
+ correctover-scan --bundle dist/app.min.js Audit a single minified/bundled JS file
420
+ correctover-scan ./cli.js Auto-detected: .js/.mjs/.cjs target → bundle mode
421
+ correctover-scan --bundle ./pkg -f sarif SARIF with file+line locations
179
422
 
180
423
  Options:
181
- -f, --format <type> Output format: text, json, sarif (default: text)
424
+ --bundle <path> Explicitly run bundle/code scan on a JS file or directory
425
+ -f, --format <type> Output format: text, json, sarif, html (default: text)
426
+ html writes a self-contained shareable report file
427
+ (correctover-scan-report-<date>.html if no -o given)
182
428
  -d, --dir <path> Directory to scan for MCP configs (default: cwd)
183
429
  -r, --recursive Recursively find MCP config files
430
+ -o, --output <file> Write the report to a file (text/json/sarif) instead of stdout
184
431
  -v, --version Show version
185
432
  -h, --help Show this help
186
433
 
434
+ Bundle mode performs signal scanning of shipped/minified code (hardcoded secrets,
435
+ plaintext endpoints, cloud metadata/SSRF, shell:true/exec, eval/Function/vm, env
436
+ credential flow, permission gates, MCP transport auth, timeouts, kill switch,
437
+ sandbox, input validation) with context-based false-positive suppression. It
438
+ locates signals at file+line; reachability/intent conclusions need manual review.
439
+
187
440
  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
441
+ npx correctover-scan Auto-detect MCP configs in cwd
442
+ npx correctover-scan --bundle . Audit the current package's code
192
443
  `);
193
444
  process.exit(0);
194
445
  } else if (arg === '--format' || arg === '-f') {
@@ -197,26 +448,60 @@ Examples:
197
448
  scanDir = args[++i];
198
449
  } else if (arg === '--recursive' || arg === '-r') {
199
450
  recursive = true;
451
+ } else if (arg === '--bundle' || arg === '-b') {
452
+ bundlePath = args[++i];
200
453
  } else if (arg === '--output' || arg === '-o') {
201
- // Output file - handled below
454
+ outFile = args[++i] || null;
202
455
  } else if (!arg.startsWith('-')) {
203
456
  configPath = arg;
204
457
  }
205
458
  }
206
459
 
207
- printBanner();
460
+ // Auto-detect bundle mode: positional .js/.mjs/.cjs file, or a directory
461
+ // that contains JS but no MCP config and looks like a package.
462
+ if (!bundlePath && configPath && fs.existsSync(configPath)) {
463
+ const st = fs.statSync(configPath);
464
+ if (st.isFile() && JS_EXT.has(path.extname(configPath).toLowerCase())) {
465
+ bundlePath = configPath;
466
+ configPath = null;
467
+ } else if (st.isDirectory() && !configPath.endsWith('.json')) {
468
+ // directory positional: prefer config mode only if a known config exists
469
+ const hasConfig = KNOWN_CONFIG_PATHS.some(rel => fs.existsSync(path.join(configPath, rel)));
470
+ if (!hasConfig) {
471
+ let hasJS = false;
472
+ try { hasJS = fs.readdirSync(configPath).some(f => JS_EXT.has(path.extname(f).toLowerCase())); } catch (e) {}
473
+ if (hasJS || fs.existsSync(path.join(configPath, 'package.json'))) {
474
+ bundlePath = configPath;
475
+ configPath = null;
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ if (bundlePath) {
482
+ return runBundleMode(bundlePath, format, outFile);
483
+ }
484
+
485
+ // In json/sarif mode all diagnostics go to stderr, keeping stdout clean.
486
+ const diag = (format === 'text' && !outFile) ? console.log : (...a) => console.error(...a);
487
+ printBanner(format === 'text' && !outFile ? 'stdout' : 'stderr');
488
+
489
+ // Text-mode payload buffer: when -o writes a report file, the report text is
490
+ // collected instead of being streamed to stdout (diagnostics stay on stderr).
491
+ const textBuffer = outFile ? [] : null;
492
+ const emit = textBuffer ? (s) => textBuffer.push(s) : (s) => console.log(s);
208
493
 
209
494
  // License check
210
495
  const status = recordCall(PRODUCT);
211
496
  if (!status.authorized) {
212
- console.log(getUpgradeMessage(status));
497
+ diag(getUpgradeMessage(status));
213
498
  process.exit(1);
214
499
  }
215
500
  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`);
501
+ diag(`${c.dim}📊 Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
502
+ diag(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}\n`);
218
503
  } else if (status.tier === 'pro') {
219
- console.log(`${c.green}✅ Pro license active — unlimited scans${c.reset}\n`);
504
+ diag(`${c.green}✅ Pro license active — unlimited scans${c.reset}\n`);
220
505
  }
221
506
 
222
507
  const filesToScan = [];
@@ -230,7 +515,7 @@ Examples:
230
515
  filesToScan.push(configPath);
231
516
  } else {
232
517
  // Auto-detect
233
- console.log(`${c.dim}Auto-detecting MCP configs in ${scanDir}...${c.reset}\n`);
518
+ diag(`${c.dim}Auto-detecting MCP configs in ${scanDir}...${c.reset}\n`);
234
519
  const found = findConfigFiles(scanDir);
235
520
  if (recursive) {
236
521
  // Walk directory tree
@@ -254,13 +539,19 @@ Examples:
254
539
  }
255
540
 
256
541
  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}`);
542
+ diag(`${c.yellow}No MCP configuration files found.${c.reset}`);
543
+ diag(`${c.dim}Searched paths: ${KNOWN_CONFIG_PATHS.join(', ')}${c.reset}`);
544
+ diag(`\n${c.dim}Config mode: correctover-scan path/to/mcp.json${c.reset}`);
545
+ const cwdHasPkg = fs.existsSync(path.join(scanDir, 'package.json'));
546
+ if (cwdHasPkg) {
547
+ diag(`${c.cyan}Detected package.json here — audit published/bundled code with: correctover-scan --bundle ${scanDir}${c.reset}`);
548
+ } else {
549
+ diag(`${c.dim}Audit an npm package / JS bundle instead: correctover-scan --bundle <path-to-package-or-js>${c.reset}`);
550
+ }
260
551
  process.exit(0);
261
552
  }
262
553
 
263
- console.log(`${c.dim}Found ${filesToScan.length} config file(s)${c.reset}\n`);
554
+ diag(`${c.dim}Found ${filesToScan.length} config file(s)${c.reset}\n`);
264
555
 
265
556
  let totalPass = 0, totalWarn = 0, totalFail = 0, totalInfo = 0;
266
557
  let allResults = [];
@@ -273,8 +564,8 @@ Examples:
273
564
  const relPath = path.relative(process.cwd(), fp) || fp;
274
565
 
275
566
  if (format === 'text') {
276
- console.log(formatResults(results, stats, relPath));
277
- console.log('');
567
+ emit(formatResults(results, stats, relPath));
568
+ emit('');
278
569
  }
279
570
 
280
571
  totalPass += stats.pass;
@@ -287,22 +578,53 @@ Examples:
287
578
  }
288
579
  }
289
580
 
290
- // JSON/SARIF output
581
+ // JSON/SARIF/HTML output
582
+ let machinePayload = null;
583
+ let htmlDefault = false;
291
584
  if (format === 'json') {
292
585
  const output = allResults.map(r => formatJSON(r.results, r.stats, r.file));
293
- console.log(output.join('\n'));
586
+ machinePayload = output.join('\n');
294
587
  } else if (format === 'sarif') {
295
588
  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));
589
+ machinePayload = JSON.stringify(sarifResults.length === 1 ? sarifResults[0] : { runs: sarifResults.flatMap(s => s.runs) }, null, 2);
590
+ } else if (format === 'html') {
591
+ const totals = { pass: totalPass, warn: totalWarn, fail: totalFail, info: totalInfo };
592
+ const denom = (totalPass + totalWarn + totalFail + totalInfo) * 10;
593
+ totals.score = denom === 0 ? 100 : Math.round(((totalPass * 10 + totalWarn * 5 + totalInfo * 7) / denom) * 100);
594
+ machinePayload = formatHTMLReport({
595
+ mode: 'MCP config scan',
596
+ target: filesToScan.length === 1 ? path.relative(process.cwd(), filesToScan[0]) || filesToScan[0] : `${filesToScan.length} config file(s)`,
597
+ items: normalizeConfigFindings(allResults), totals,
598
+ checksLabel: '14 config checks mapped to OWASP AISVS 1.0',
599
+ version: VERSION,
600
+ });
601
+ if (!outFile) { outFile = defaultReportPath(); htmlDefault = true; }
297
602
  }
298
603
 
299
604
  // Summary
300
605
  if (filesToScan.length > 1 && format === 'text') {
301
606
  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}`);
607
+ emit(`${c.bold}═══ Summary ═══${c.reset}`);
608
+ emit(` Files scanned: ${filesToScan.length}`);
609
+ emit(` Total score: ${totalScore}/100`);
610
+ emit(` ${c.green}✓ ${totalPass}${c.reset} ${c.yellow}⚠ ${totalWarn}${c.reset} ${c.red}✗ ${totalFail}${c.reset} ${c.blue}ℹ ${totalInfo}${c.reset}`);
611
+ }
612
+
613
+ // -o: write the report payload to a file; otherwise stream to stdout
614
+ if (outFile) {
615
+ const payload = machinePayload || textBuffer.join('\n');
616
+ try {
617
+ fs.writeFileSync(outFile, payload + '\n');
618
+ diag(`${c.green}✅ Report written to ${outFile}${c.reset}`);
619
+ if (htmlDefault) {
620
+ diag(`${c.dim}Shareable HTML report — forward it to your team. ${failHint(totalFail)}${c.reset}`);
621
+ }
622
+ } catch (e) {
623
+ console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
624
+ process.exit(1);
625
+ }
626
+ } else if (machinePayload) {
627
+ console.log(machinePayload);
306
628
  }
307
629
 
308
630
  // 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.5.0",
4
- "description": "MCP security scanner1 command, 14 OWASP AISVS 1.0 checks. Detect RCE, SSRF, credential exposure. Free: 30 checks/month.",
3
+ "version": "1.7.0",
4
+ "description": "Correctover Security ScannerCCS 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,52 @@
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",
60
+ "core/report.js",
42
61
  "core/license.js",
43
62
  "README.md"
44
63
  ],