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/core/license.js CHANGED
@@ -1,11 +1,7 @@
1
1
  /**
2
- * License validator freemium hook model.
3
- *
4
- * Free: unlimited scanning (see all risks)
5
- * Pro: fix recommendations, auto-heal, reports, history
6
- *
7
- * The hook: users see ALL their problems for free.
8
- * The paywall: solutions are locked.
2
+ * License validator for correctover-scan NPM.
3
+ * Mirrors the Python LicenseValidator: 50 free scans/day, CORRECTOVER_LICENSE_KEY unlocks.
4
+ * Supports CV-TRL/CV-PRO (FC) and COV- (Cloud) key formats.
9
5
  */
10
6
 
11
7
  const fs = require('fs');
@@ -13,8 +9,8 @@ const path = require('path');
13
9
  const crypto = require('crypto');
14
10
  const os = require('os');
15
11
 
12
+ const FREE_LIMIT_PER_DAY = 50;
16
13
  const STATE_FILE = path.join(os.homedir(), '.correctover', 'license.json');
17
- const FREE_FIX_PREVIEW = 2;
18
14
 
19
15
  function loadState() {
20
16
  try {
@@ -22,7 +18,7 @@ function loadState() {
22
18
  return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
23
19
  }
24
20
  } catch (e) {}
25
- return { products: {}, license_key: null, installed_at: Date.now() / 1000, scan_history: [] };
21
+ return { products: {}, license_key: null, installed_at: Date.now() / 1000 };
26
22
  }
27
23
 
28
24
  function saveState(state) {
@@ -33,8 +29,13 @@ function saveState(state) {
33
29
  }
34
30
 
35
31
  function getProductState(state, product) {
32
+ const today = new Date().toISOString().slice(0, 10);
36
33
  if (!state.products[product]) {
37
- state.products[product] = { total_scans: 0, total_risks_found: 0, first_scan: Date.now()/1000, last_scan: null };
34
+ state.products[product] = { calls_today: 0, date: today, total_calls: 0 };
35
+ }
36
+ if (state.products[product].date !== today) {
37
+ state.products[product].calls_today = 0;
38
+ state.products[product].date = today;
38
39
  }
39
40
  return state.products[product];
40
41
  }
@@ -42,6 +43,7 @@ function getProductState(state, product) {
42
43
  function verifyLicenseKey(key) {
43
44
  if (!key || key.length < 12) return false;
44
45
 
46
+ // COV-<product>-<hash> (Cloud HMAC)
45
47
  if (key.startsWith('COV-')) {
46
48
  const parts = key.split('-');
47
49
  if (parts.length < 3) return false;
@@ -51,6 +53,7 @@ function verifyLicenseKey(key) {
51
53
  return parts[parts.length - 1].startsWith(expected);
52
54
  }
53
55
 
56
+ // CV-TRL-<base64> / CV-PRO-<base64> (FC XunhuPay)
54
57
  if (key.startsWith('CV-')) {
55
58
  const parts = key.split('-', 2);
56
59
  if (parts.length < 3) return false;
@@ -70,94 +73,56 @@ function verifyLicenseKey(key) {
70
73
 
71
74
  function checkLicense(product) {
72
75
  const state = loadState();
76
+ const ps = getProductState(state, product);
73
77
  const licenseKey = state.license_key || process.env.CORRECTOVER_LICENSE_KEY;
74
78
 
75
79
  if (licenseKey && verifyLicenseKey(licenseKey)) {
76
80
  return {
81
+ authorized: true,
77
82
  tier: 'pro',
78
- can_scan: true,
79
- can_fix: true,
80
- can_report: true,
81
- can_heal: true,
82
- can_history: true,
83
- fix_preview: Infinity,
83
+ calls_remaining: Infinity,
84
+ calls_today: ps.calls_today,
85
+ limit: Infinity,
84
86
  };
85
87
  }
86
88
 
89
+ const remaining = Math.max(0, FREE_LIMIT_PER_DAY - ps.calls_today);
87
90
  return {
91
+ authorized: remaining > 0,
88
92
  tier: 'free',
89
- can_scan: true,
90
- can_fix: false,
91
- can_report: false,
92
- can_heal: false,
93
- can_history: false,
94
- fix_preview: FREE_FIX_PREVIEW,
93
+ calls_remaining: remaining,
94
+ calls_today: ps.calls_today,
95
+ limit: FREE_LIMIT_PER_DAY,
95
96
  };
96
97
  }
97
98
 
98
- function recordScan(product, risksFound) {
99
+ function recordCall(product) {
99
100
  const state = loadState();
100
- const ps = getProductState(state, product);
101
- ps.total_scans += 1;
102
- ps.total_risks_found += risksFound || 0;
103
- ps.last_scan = Date.now() / 1000;
104
-
105
- const history = state.scan_history || [];
106
- history.push({ time: Date.now()/1000, product, risks: risksFound || 0 });
107
-
108
101
  const status = checkLicense(product);
109
- if (status.tier === 'free' && history.length > 1) {
110
- state.scan_history = history.slice(-1);
111
- } else {
112
- state.scan_history = history;
113
- }
102
+ if (!status.authorized) return status;
114
103
 
104
+ const ps = getProductState(state, product);
105
+ ps.calls_today += 1;
106
+ ps.total_calls = (ps.total_calls || 0) + 1;
115
107
  saveState(state);
116
- return checkLicense(product);
117
- }
118
108
 
119
- function getFixCTA(totalRisks, hiddenRisks) {
120
- if (hiddenRisks <= 0) return '';
121
- const shown = totalRisks - hiddenRisks;
122
- return [
123
- '',
124
- '━'.repeat(55),
125
- `🔒 ${hiddenRisks} fix recommendation(s) locked.`,
126
- ` You have ${totalRisks} risk(s) but can only see ${shown} fix(es).`,
127
- '',
128
- '🛡️ Upgrade to Pro to unlock:',
129
- ` ✓ Fix recommendations for all ${totalRisks} risk(s)`,
130
- ' ✓ Auto-heal (84.1% issues resolved automatically)',
131
- ' ✓ HTML/PDF audit reports',
132
- ' ✓ Scan history & tracking',
133
- '━'.repeat(55),
134
- ' → https://correctover.com/checkout',
135
- ' → export CORRECTOVER_LICENSE_KEY=<your-key>',
136
- '━'.repeat(55),
137
- ].join('\n');
109
+ status.calls_remaining = Math.max(0, status.limit - ps.calls_today);
110
+ status.calls_today = ps.calls_today;
111
+ return status;
138
112
  }
139
113
 
140
- function getNoRiskCTA() {
141
- return [
142
- '',
143
- '━'.repeat(55),
144
- '✅ No risks found in this scan.',
145
- '',
146
- '🛡️ Stay protected with Pro:',
147
- ' ✓ Continuous monitoring (auto-scan on changes)',
148
- ' ✓ Auto-heal when risks appear',
149
- ' ✓ Compliance reports (OAuth 2.1, CCS v1.0)',
150
- '━'.repeat(55),
151
- ' → https://correctover.com/checkout',
152
- '━'.repeat(55),
153
- ].join('\n');
114
+ function getUpgradeMessage(status) {
115
+ if (status.tier !== 'free') return '';
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`;
118
+ }
119
+ return `\n📊 Free tier: ${status.calls_remaining} scans remaining today.\n Upgrade: https://correctover.com/checkout\n`;
154
120
  }
155
121
 
156
122
  module.exports = {
157
- FREE_FIX_PREVIEW,
123
+ FREE_LIMIT_PER_DAY,
158
124
  checkLicense,
159
- recordScan,
125
+ recordCall,
126
+ getUpgradeMessage,
160
127
  verifyLicenseKey,
161
- getFixCTA,
162
- getNoRiskCTA,
163
128
  };
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/core/scanner.js CHANGED
File without changes