correctover-scan 1.6.0 → 1.7.1
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 +3 -0
- package/core/report.js +227 -0
- package/index.js +52 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# correctover-scan
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
[](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
|

|
package/core/report.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
+
.gateway{background:rgba(79,124,255,.08);border:1px solid var(--accent);border-radius:12px;padding:14px 18px;margin-bottom:20px;font-size:14px}
|
|
126
|
+
.gateway a{color:var(--accent);font-weight:700;text-decoration:none;white-space:nowrap}
|
|
127
|
+
.disclaimer{background:rgba(240,185,85,.06);border:1px solid rgba(240,185,85,.4);border-radius:12px;padding:18px 20px;margin:22px 0;font-size:12.5px;color:var(--muted)}
|
|
128
|
+
.disclaimer h2{font-size:14px;color:var(--warn);border:none;margin:0 0 10px;padding:0}
|
|
129
|
+
.disclaimer p{margin-bottom:8px}
|
|
130
|
+
.disclaimer p:last-child{margin-bottom:0}
|
|
131
|
+
.disclaimer strong{color:var(--text)}
|
|
132
|
+
.printbtn{position:fixed;top:14px;right:14px;background:var(--accent);color:#fff;border:none;border-radius:10px;padding:10px 16px;font-size:13px;font-weight:700;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.35);z-index:10}
|
|
133
|
+
@media print{
|
|
134
|
+
:root{--bg:#ffffff;--card:#ffffff;--line:#bbbbbb;--text:#111111;--muted:#333333;--accent:#0a4dd6;--ok:#0a7a52;--warn:#8a6100;--bad:#b32020}
|
|
135
|
+
body{background:#fff;padding:0}
|
|
136
|
+
.printbtn{display:none !important}
|
|
137
|
+
.hero,.finding,.note,.gateway,.disclaimer,.next .card2{break-inside:avoid}
|
|
138
|
+
.disclaimer{background:#fffbe8 !important;border-color:#999 !important;color:#000 !important}
|
|
139
|
+
.disclaimer h2{color:#000 !important}.disclaimer strong{color:#000 !important}
|
|
140
|
+
.f-snip{background:#f6f6f6 !important;color:#111 !important}
|
|
141
|
+
code{background:#f0f0f0 !important}
|
|
142
|
+
.next a{text-decoration:none}
|
|
143
|
+
}
|
|
144
|
+
</style></head>
|
|
145
|
+
<body><div class="wrap">
|
|
146
|
+
|
|
147
|
+
<button class="printbtn" onclick="window.print()">🖨 Print / Save as PDF</button>
|
|
148
|
+
|
|
149
|
+
<div class="gateway">
|
|
150
|
+
This automated scan maps surface patterns to <strong>OWASP AISVS 1.0</strong> controls — it is not a certification or an audit opinion.
|
|
151
|
+
Need an <strong>official compliance report for vendor acceptance</strong>, an AISVS mapping workbook, or signed version-provenance records?
|
|
152
|
+
👉 <a href="https://dshcorrectover.github.io/agent-audit/scan.html#enterprise">Enterprise report & provenance options (official entry)</a>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div class="hero">
|
|
156
|
+
<div class="brand">Correctover <span>· AI Reliability™ — Agent Runtime Assurance</span></div>
|
|
157
|
+
<h1>Agent Security Scan Report</h1>
|
|
158
|
+
<div class="sub">Target: <strong>${esc(target)}</strong> · Mode: ${esc(mode)} · correctover-scan v${esc(ver)} (${esc(checksLabel)}) · ${esc(when)}</div>
|
|
159
|
+
<div class="score-row">
|
|
160
|
+
<div>
|
|
161
|
+
<div class="score">${score}<span style="font-size:24px;color:var(--muted)">/100</span></div>
|
|
162
|
+
<div class="grade" style="background:${grade[1]}">${grade[0]}</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="counts">
|
|
165
|
+
<div class="c-pass"><b>${pass}</b>passed</div>
|
|
166
|
+
<div class="c-warn"><b>${warn}</b>warnings</div>
|
|
167
|
+
<div class="c-fail"><b>${fail}</b>critical</div>
|
|
168
|
+
<div class="c-info"><b>${info}</b>info</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<h2>Critical & warning findings (${active.length})</h2>
|
|
174
|
+
${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>'}
|
|
175
|
+
|
|
176
|
+
${infos.length ? `<h2>Informational signals (${infos.length})</h2>${infos.map(card).join('')}` : ''}
|
|
177
|
+
|
|
178
|
+
${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>` : ''}
|
|
179
|
+
|
|
180
|
+
<div class="note">
|
|
181
|
+
<strong>Scope & limits:</strong> this report is produced by static signal scanning (${esc(checksLabel)}).
|
|
182
|
+
It locates risk-bearing patterns with file/line locations; it does not execute code, prove reachability or
|
|
183
|
+
exploitability, or judge semantic intent. Fail/warn items are the input for manual deep review, not a verdict
|
|
184
|
+
of compromise. Nothing in this scan leaves your machine — no network calls, no telemetry.
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="disclaimer">
|
|
188
|
+
<h2>⚠ Important notice — read before sharing or filing this report</h2>
|
|
189
|
+
<p><strong>1. Not an audit, certification, or compliance attestation.</strong> This document is output of an automated static signal scan (${esc(checksLabel)}). It is <strong>not</strong> a security audit, penetration test, certification, or statement of compliance with any standard or regulation. A score of 100 or the absence of critical findings does <strong>not</strong> mean the code is vulnerability-free, secure, or compliant — it means only that the listed patterns were not detected in the scanned snapshot. Static analysis cannot prove the absence of reachable vulnerabilities; semantic intent, reachability, exploitability, and runtime behavior are not determined.</p>
|
|
190
|
+
<p><strong>2. AISVS mapping is navigational only.</strong> OWASP® and AISVS are trademarks of the OWASP Foundation. Correctover is not affiliated with or endorsed by OWASP. References such as <code>C10.1</code> are our own mapping of checks to control families in OWASP AISVS v1.0 (June 2026), provided as a navigation aid; OWASP does not certify products or vendors, and this report must not be presented as an OWASP or AISVS certification.</p>
|
|
191
|
+
<p><strong>3. Not a substitute for professional review.</strong> Do not rely on this document as the sole basis for acceptance, procurement, release, insurance, regulatory, or other consequential decisions. High-assurance use cases require an engagement performed by qualified personnel under an agreed scope and terms (such as Correctover's 116-check manual deep audit, delivered under a signed agreement).</p>
|
|
192
|
+
<p><strong>4. Transparency-log anchors prove existence, not safety.</strong> Sigstore Rekor entries and any content-addressed manifest attest only that a byte-for-byte snapshot existed at a point in time. They make no statement about code safety, quality, ownership, or legal rights.</p>
|
|
193
|
+
<p><strong>5. AS-IS.</strong> The scan and report are provided "as is", without warranty of any kind, express or implied, including merchantability or fitness for a particular purpose. To the maximum extent permitted by law, Correctover shall not be liable for any damages arising from their use. You are responsible for independently verifying findings before acting on them. Automated results may include false positives and false negatives.</p>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<h2>Go further</h2>
|
|
197
|
+
<div class="next">
|
|
198
|
+
<a href="https://dshcorrectover.github.io/agent-audit/scan.html"><div class="card2">
|
|
199
|
+
<h3>Free deep scan in your browser →</h3>
|
|
200
|
+
<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>
|
|
201
|
+
</div></a>
|
|
202
|
+
<a href="https://dshcorrectover.github.io/agent-audit/"><div class="card2">
|
|
203
|
+
<h3>116-check manual deep audit →</h3>
|
|
204
|
+
<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>
|
|
205
|
+
</div></a>
|
|
206
|
+
<a href="https://github.com/DSHCorrectover/correctover-scan-action"><div class="card2">
|
|
207
|
+
<h3>Scan on every push →</h3>
|
|
208
|
+
<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>
|
|
209
|
+
</div></a>
|
|
210
|
+
<a href="https://github.com/DSHCorrectover/code-birth-certificate"><div class="card2">
|
|
211
|
+
<h3>Anchor what shipped →</h3>
|
|
212
|
+
<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>
|
|
213
|
+
</div></a>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<footer>
|
|
217
|
+
Generated by <strong>correctover-scan</strong> v${esc(ver)} · static analysis, runs entirely locally ·
|
|
218
|
+
<a href="https://correctover.com" style="color:var(--accent)">correctover.com</a> ·
|
|
219
|
+
<a href="https://github.com/DSHCorrectover" style="color:var(--accent)">github.com/DSHCorrectover</a>
|
|
220
|
+
</footer>
|
|
221
|
+
</div>
|
|
222
|
+
</body></html>`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
226
|
+
module.exports = { esc, failHint, normalizeConfigFindings, normalizeBundleFindings, formatHTMLReport };
|
|
227
|
+
}
|
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.
|
|
17
|
+
const VERSION = '1.7.1';
|
|
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.
|
|
3
|
+
"version": "1.7.1",
|
|
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
|
],
|