correctover-scan 1.5.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/LICENSE +21 -0
- package/README.md +112 -27
- package/core/bundle-scanner.js +775 -0
- package/core/license.js +40 -75
- package/core/scanner.js +0 -0
- package/index.js +302 -29
- package/package.json +26 -8
package/core/license.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* License validator
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
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] = {
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
123
|
+
FREE_LIMIT_PER_DAY,
|
|
158
124
|
checkLicense,
|
|
159
|
-
|
|
125
|
+
recordCall,
|
|
126
|
+
getUpgradeMessage,
|
|
160
127
|
verifyLicenseKey,
|
|
161
|
-
getFixCTA,
|
|
162
|
-
getNoRiskCTA,
|
|
163
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.
|
|
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
|
|
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/
|
|
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 [
|
|
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
|
|
189
|
-
correctover-scan
|
|
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
|
-
|
|
421
|
+
outFile = args[++i] || null;
|
|
202
422
|
} else if (!arg.startsWith('-')) {
|
|
203
423
|
configPath = arg;
|
|
204
424
|
}
|
|
205
425
|
}
|
|
206
426
|
|
|
207
|
-
|
|
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
|
-
|
|
464
|
+
diag(getUpgradeMessage(status));
|
|
213
465
|
process.exit(1);
|
|
214
466
|
}
|
|
215
467
|
if (status.tier === 'free') {
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
552
|
+
machinePayload = output.join('\n');
|
|
294
553
|
} else if (format === 'sarif') {
|
|
295
554
|
const sarifResults = allResults.map(r => formatSARIF(r.results, r.file));
|
|
296
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|