correctover-scan 1.6.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/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # correctover-scan
2
2
 
3
+
4
+ [![Rekor anchored](https://img.shields.io/badge/Rekor%20anchored-logIndex%202697131671-1455A3)](https://search.sigstore.dev/?logIndex=2697131671)
5
+
3
6
  > Security scanner for MCP servers and AI agents — detects credential exposure, SSRF, command injection risk and missing auth across your MCP configuration. 14 checks mapped to OWASP AISVS 1.0. Run anywhere with `npx correctover-scan`.
4
7
 
5
8
  ![npm](https://img.shields.io/npm/v/correctover-scan)
package/core/report.js ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Correctover CCS Security Scanner - HTML report generator
3
+ * Zero dependencies (browser-compatible). Shared by CLI / GitHub Action /
4
+ * browser build (https://dshcorrectover.github.io/agent-audit/scan.html).
5
+ */
6
+
7
+ function esc(s) {
8
+ return String(s == null ? '' : s)
9
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
11
+ }
12
+
13
+ function failHint(failCount) {
14
+ return failCount > 0
15
+ ? 'Critical findings are the entry point for the 116-check manual deep audit — see the report footer.'
16
+ : 'Clean snapshot — re-run on every change.';
17
+ }
18
+
19
+ function normalizeConfigFindings(allResults) {
20
+ const items = [];
21
+ for (const r of allResults) {
22
+ for (const chk of r.results) {
23
+ items.push({
24
+ file: r.file, checkId: chk.id || '', checkName: chk.name, category: chk.category || '',
25
+ aisvs: chk.aisvs || '', status: chk.status, severity: chk.severity || chk.status,
26
+ line: null, message: chk.status === 'pass' ? 'Check passed.' : (chk.fix || chk.name),
27
+ snippet: '', fix: chk.fix || '',
28
+ });
29
+ }
30
+ }
31
+ return items;
32
+ }
33
+
34
+ function normalizeBundleFindings(bundle) {
35
+ const items = [];
36
+ for (const fr of bundle.files) {
37
+ for (const r of fr.results) {
38
+ for (const f of r.findings) {
39
+ if (f.suppressed) continue;
40
+ items.push({
41
+ file: fr.file, checkId: r.id, checkName: r.name, category: r.category || '',
42
+ aisvs: r.aisvs || '', status: f.severity === 'fail' ? 'fail' : f.severity === 'warn' ? 'warn' : 'info',
43
+ severity: f.severity, line: f.line || null, message: f.message,
44
+ snippet: f.snippet || '', fix: r.fix || '',
45
+ });
46
+ }
47
+ }
48
+ }
49
+ return items;
50
+ }
51
+
52
+ function formatHTMLReport(opts) {
53
+ const { mode, target, items, totals, checksLabel, version } = opts;
54
+ const ver = version || '0.0.0';
55
+ const { score, pass, warn, fail, info } = totals;
56
+ const grade = fail > 0 || score < 60 ? ['CRITICAL RISK', '#ff5d5d']
57
+ : warn > 0 || score < 85 ? ['REVIEW RECOMMENDED', '#f0b955']
58
+ : ['NO CRITICAL FINDINGS', '#37d0a0'];
59
+ const when = new Date().toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
60
+
61
+ const sevRank = { fail: 0, critical: 0, high: 1, warn: 2, medium: 2, low: 3, info: 4, pass: 5 };
62
+ const sorted = [...items].sort((a, b) => (sevRank[a.status] ?? 9) - (sevRank[b.status] ?? 9));
63
+ const active = sorted.filter(i => i.status === 'fail' || i.status === 'warn');
64
+ const infos = sorted.filter(i => i.status === 'info');
65
+ const passed = sorted.filter(i => i.status === 'pass');
66
+
67
+ const card = (i) => `
68
+ <div class="finding sev-${esc(i.status)}">
69
+ <div class="f-head">
70
+ <span class="badge b-${esc(i.status)}">${i.status === 'fail' ? 'CRITICAL' : i.status === 'warn' ? 'WARNING' : 'INFO'}</span>
71
+ <span class="f-title">${esc(i.checkName)}</span>
72
+ <span class="f-loc">${esc(i.file)}${i.line ? ':' + esc(i.line) : ''}</span>
73
+ </div>
74
+ <div class="f-meta">${i.checkId ? '<code>' + esc(i.checkId) + '</code> · ' : ''}${esc(i.category)}${i.aisvs ? ' · maps to <code>' + esc(i.aisvs) + '</code>' : ''}</div>
75
+ <div class="f-msg">${esc(i.message)}</div>
76
+ ${i.snippet ? `<pre class="f-snip">${esc(i.snippet)}</pre>` : ''}
77
+ ${i.fix && i.status !== 'pass' ? `<div class="f-fix"><strong>Fix:</strong> ${esc(i.fix)}</div>` : ''}
78
+ </div>`;
79
+
80
+ return `<!DOCTYPE html>
81
+ <html lang="en"><head><meta charset="utf-8">
82
+ <meta name="viewport" content="width=device-width, initial-scale=1">
83
+ <title>Agent Security Scan Report — ${esc(target)}</title>
84
+ <meta name="description" content="Correctover agent security scan report for ${esc(target)}">
85
+ <style>
86
+ :root{--bg:#0b1020;--card:#141d38;--line:#263156;--text:#e8ecf8;--muted:#9aa7c7;--accent:#4f7cff;--ok:#37d0a0;--warn:#f0b955;--bad:#ff5d5d;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
87
+ *{box-sizing:border-box;margin:0;padding:0}
88
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;padding:32px 16px}
89
+ .wrap{max-width:880px;margin:0 auto}
90
+ .hero{background:linear-gradient(135deg,#16203f,#101736);border:1px solid var(--line);border-radius:16px;padding:28px 30px;margin-bottom:20px}
91
+ .brand{font-size:13px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);font-weight:700}
92
+ .brand span{color:var(--muted);font-weight:400;letter-spacing:.05em;text-transform:none}
93
+ h1{font-size:24px;margin:10px 0 4px}
94
+ .sub{color:var(--muted);font-size:14px}
95
+ .score-row{display:flex;align-items:center;gap:28px;margin-top:22px;flex-wrap:wrap}
96
+ .score{font-size:56px;font-weight:800;line-height:1}
97
+ .grade{display:inline-block;padding:6px 14px;border-radius:999px;font-weight:700;font-size:13px;margin-top:8px;color:#0b1020}
98
+ .counts{display:flex;gap:18px;flex-wrap:wrap;font-size:14px}
99
+ .counts b{font-size:20px;display:block}
100
+ .c-pass b{color:var(--ok)}.c-warn b{color:var(--warn)}.c-fail b{color:var(--bad)}.c-info b{color:var(--accent)}
101
+ h2{font-size:16px;margin:28px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--line)}
102
+ .finding{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--muted);border-radius:10px;padding:14px 16px;margin-bottom:10px}
103
+ .finding.sev-fail{border-left-color:var(--bad)}.finding.sev-warn{border-left-color:var(--warn)}.finding.sev-info{border-left-color:var(--accent)}
104
+ .f-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
105
+ .f-title{font-weight:700;font-size:15px}
106
+ .f-loc{margin-left:auto;color:var(--muted);font-family:var(--mono);font-size:12px}
107
+ .badge{padding:2px 9px;border-radius:6px;font-size:11px;font-weight:700;color:#0b1020}
108
+ .b-fail{background:var(--bad)}.b-warn{background:var(--warn)}.b-info{background:var(--accent);color:#fff}
109
+ .f-meta{color:var(--muted);font-size:12px;margin:4px 0}
110
+ code{font-family:var(--mono);background:#0d1430;padding:1px 6px;border-radius:4px;font-size:12px}
111
+ .f-msg{font-size:14px;margin-top:6px}
112
+ .f-snip{background:#080d1f;border:1px solid var(--line);border-radius:8px;padding:10px 12px;font-family:var(--mono);font-size:12px;color:#c7d2f0;overflow-x:auto;margin-top:8px;white-space:pre-wrap;word-break:break-all}
113
+ .f-fix{margin-top:8px;font-size:13px;color:var(--ok)}
114
+ .note{background:#101736;border:1px solid var(--line);border-radius:10px;padding:14px 16px;font-size:13px;color:var(--muted);margin:18px 0}
115
+ .next{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:12px;margin-top:12px}
116
+ .next a{text-decoration:none;color:inherit;display:block}
117
+ .next .card2{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px;transition:border-color .15s}
118
+ .next .card2:hover{border-color:var(--accent)}
119
+ .next h3{font-size:15px;color:var(--accent);margin-bottom:6px}
120
+ .next p{font-size:13px;color:var(--muted)}
121
+ footer{margin-top:28px;padding-top:16px;border-top:1px solid var(--line);color:var(--muted);font-size:12px;text-align:center}
122
+ details{margin-top:8px}summary{cursor:pointer;color:var(--muted);font-size:13px}
123
+ .pass-list{font-size:13px;color:var(--muted);columns:2;margin-top:10px}
124
+ .pass-list div{break-inside:avoid;padding:2px 0}
125
+ </style></head>
126
+ <body><div class="wrap">
127
+
128
+ <div class="hero">
129
+ <div class="brand">Correctover <span>· AI Reliability™ — Agent Runtime Assurance</span></div>
130
+ <h1>Agent Security Scan Report</h1>
131
+ <div class="sub">Target: <strong>${esc(target)}</strong> &nbsp;·&nbsp; Mode: ${esc(mode)} &nbsp;·&nbsp; correctover-scan v${esc(ver)} (${esc(checksLabel)}) &nbsp;·&nbsp; ${esc(when)}</div>
132
+ <div class="score-row">
133
+ <div>
134
+ <div class="score">${score}<span style="font-size:24px;color:var(--muted)">/100</span></div>
135
+ <div class="grade" style="background:${grade[1]}">${grade[0]}</div>
136
+ </div>
137
+ <div class="counts">
138
+ <div class="c-pass"><b>${pass}</b>passed</div>
139
+ <div class="c-warn"><b>${warn}</b>warnings</div>
140
+ <div class="c-fail"><b>${fail}</b>critical</div>
141
+ <div class="c-info"><b>${info}</b>info</div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <h2>Critical &amp; warning findings (${active.length})</h2>
147
+ ${active.length ? active.map(card).join('') : '<div class="note">✅ No fail- or warning-level findings. This is a signal scan over the current snapshot — keep scanning on every change.</div>'}
148
+
149
+ ${infos.length ? `<h2>Informational signals (${infos.length})</h2>${infos.map(card).join('')}` : ''}
150
+
151
+ ${passed.length ? `<details><summary>${passed.length} checks passed — expand to list</summary><div class="pass-list">${passed.map(i => `<div>✓ ${esc(i.checkName)} <code>${esc(i.checkId)}</code></div>`).join('')}</div></details>` : ''}
152
+
153
+ <div class="note">
154
+ <strong>Scope &amp; limits:</strong> this report is produced by static signal scanning (${esc(checksLabel)}).
155
+ It locates risk-bearing patterns with file/line locations; it does not execute code, prove reachability or
156
+ exploitability, or judge semantic intent. Fail/warn items are the input for manual deep review, not a verdict
157
+ of compromise. Nothing in this scan leaves your machine — no network calls, no telemetry.
158
+ </div>
159
+
160
+ <h2>Go further</h2>
161
+ <div class="next">
162
+ <a href="https://dshcorrectover.github.io/agent-audit/scan.html"><div class="card2">
163
+ <h3>Free deep scan in your browser →</h3>
164
+ <p>Want the same scan as an <strong>instant, shareable report</strong> for any project? The <strong>free online scanner</strong> runs 100% in your browser — nothing is uploaded. Scan MCP configs or entire codebases, download the branded report, forward it to your team.</p>
165
+ </div></a>
166
+ <a href="https://dshcorrectover.github.io/agent-audit/"><div class="card2">
167
+ <h3>116-check manual deep audit →</h3>
168
+ <p>Automated scanning covers surface signals. A Correctover manual audit applies the <strong>116-check semantic-intent methodology</strong> over 5 days, tracing reachability and exploit paths in the real code. If we find no critical-severity issue, you pay nothing.</p>
169
+ </div></a>
170
+ <a href="https://github.com/DSHCorrectover/correctover-scan-action"><div class="card2">
171
+ <h3>Scan on every push →</h3>
172
+ <p>The <strong>correctover-scan GitHub Action</strong> runs this scan in CI, fails the build on critical findings, and uploads SARIF to GitHub code scanning — so regressions never reach production.</p>
173
+ </div></a>
174
+ <a href="https://github.com/DSHCorrectover/code-birth-certificate"><div class="card2">
175
+ <h3>Anchor what shipped →</h3>
176
+ <p>The <strong>Code Birth Certificate</strong> action signs a content-addressed manifest and anchors it in the Sigstore Rekor transparency log — public, independently verifiable proof of exactly what shipped and when. No account needed to verify.</p>
177
+ </div></a>
178
+ </div>
179
+
180
+ <footer>
181
+ Generated by <strong>correctover-scan</strong> v${esc(ver)} · static analysis, runs entirely locally ·
182
+ <a href="https://correctover.com" style="color:var(--accent)">correctover.com</a> ·
183
+ <a href="https://github.com/DSHCorrectover" style="color:var(--accent)">github.com/DSHCorrectover</a>
184
+ </footer>
185
+ </div>
186
+ </body></html>`;
187
+ }
188
+
189
+ if (typeof module !== 'undefined' && module.exports) {
190
+ module.exports = { esc, failHint, normalizeConfigFindings, normalizeBundleFindings, formatHTMLReport };
191
+ }
package/index.js CHANGED
@@ -12,8 +12,9 @@ const path = require('path');
12
12
  const { runScan, parseConfig, KNOWN_CONFIG_PATHS } = require('./core/scanner');
13
13
  const { runBundleScan, discoverBundleFiles } = require('./core/bundle-scanner');
14
14
  const { recordCall, getUpgradeMessage } = require('./core/license');
15
+ const { formatHTMLReport, normalizeConfigFindings, normalizeBundleFindings, failHint } = require('./core/report');
15
16
 
16
- const VERSION = '1.6.0';
17
+ const VERSION = '1.7.0';
17
18
  const PRODUCT = 'correctover-scan';
18
19
  const JS_EXT = new Set(['.js', '.mjs', '.cjs', '.ts']);
19
20
 
@@ -291,6 +292,20 @@ function formatJSON(results, stats, filename) {
291
292
  return JSON.stringify({ scanner: 'correctover-scan', version: VERSION, file: filename, stats, results }, null, 2);
292
293
  }
293
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
+
294
309
  function runBundleMode(target, format, outFile) {
295
310
  // All diagnostics/banners go to stderr so that stdout carries only the
296
311
  // machine-readable payload for json/sarif consumers.
@@ -335,10 +350,23 @@ function runBundleMode(target, format, outFile) {
335
350
  const label = path.relative(process.cwd(), abs) || abs;
336
351
 
337
352
  let payload;
353
+ let htmlDefault = false;
338
354
  if (format === 'json') {
339
355
  payload = formatBundleJSON(bundle, label);
340
356
  } else if (format === 'sarif') {
341
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; }
342
370
  } else {
343
371
  payload = formatBundleResults(bundle, label);
344
372
  }
@@ -346,6 +374,9 @@ function runBundleMode(target, format, outFile) {
346
374
  try {
347
375
  fs.writeFileSync(outFile, payload + '\n');
348
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
+ }
349
380
  } catch (e) {
350
381
  console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
351
382
  process.exit(1);
@@ -391,7 +422,9 @@ Bundle / published-package code mode (v1.4.0+):
391
422
 
392
423
  Options:
393
424
  --bundle <path> Explicitly run bundle/code scan on a JS file or directory
394
- -f, --format <type> Output format: text, json, sarif (default: text)
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)
395
428
  -d, --dir <path> Directory to scan for MCP configs (default: cwd)
396
429
  -r, --recursive Recursively find MCP config files
397
430
  -o, --output <file> Write the report to a file (text/json/sarif) instead of stdout
@@ -545,14 +578,27 @@ Examples:
545
578
  }
546
579
  }
547
580
 
548
- // JSON/SARIF output
581
+ // JSON/SARIF/HTML output
549
582
  let machinePayload = null;
583
+ let htmlDefault = false;
550
584
  if (format === 'json') {
551
585
  const output = allResults.map(r => formatJSON(r.results, r.stats, r.file));
552
586
  machinePayload = output.join('\n');
553
587
  } else if (format === 'sarif') {
554
588
  const sarifResults = allResults.map(r => formatSARIF(r.results, r.file));
555
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; }
556
602
  }
557
603
 
558
604
  // Summary
@@ -570,6 +616,9 @@ Examples:
570
616
  try {
571
617
  fs.writeFileSync(outFile, payload + '\n');
572
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
+ }
573
622
  } catch (e) {
574
623
  console.error(`${c.red}Error writing output file: ${e.message}${c.reset}`);
575
624
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "correctover-scan",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
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": {
@@ -57,6 +57,7 @@
57
57
  "index.js",
58
58
  "core/scanner.js",
59
59
  "core/bundle-scanner.js",
60
+ "core/report.js",
60
61
  "core/license.js",
61
62
  "README.md"
62
63
  ],