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/LICENSE +21 -0
- package/README.md +115 -27
- package/core/bundle-scanner.js +775 -0
- package/core/license.js +40 -75
- package/core/report.js +191 -0
- package/core/scanner.js +0 -0
- package/index.js +353 -31
- package/package.json +27 -8
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.
|
|
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
|
|
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/
|
|
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 [
|
|
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
|
-
|
|
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
|
|
189
|
-
correctover-scan
|
|
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
|
-
|
|
454
|
+
outFile = args[++i] || null;
|
|
202
455
|
} else if (!arg.startsWith('-')) {
|
|
203
456
|
configPath = arg;
|
|
204
457
|
}
|
|
205
458
|
}
|
|
206
459
|
|
|
207
|
-
|
|
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
|
-
|
|
497
|
+
diag(getUpgradeMessage(status));
|
|
213
498
|
process.exit(1);
|
|
214
499
|
}
|
|
215
500
|
if (status.tier === 'free') {
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
586
|
+
machinePayload = output.join('\n');
|
|
294
587
|
} else if (format === 'sarif') {
|
|
295
588
|
const sarifResults = allResults.map(r => formatSARIF(r.results, r.file));
|
|
296
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Correctover Security Scanner — CCS audit for MCP configurations AND published JS bundles/npm packages. Detects hardcoded secrets, RCE, SSRF, credential hijacking against OWASP AISVS 1.0.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"correctover-scan": "./index.js"
|
|
@@ -12,33 +12,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
|
-
"
|
|
27
|
-
"
|
|
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/
|
|
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/
|
|
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
|
],
|