correctover-scan 2.1.3 → 3.0.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.
@@ -0,0 +1,128 @@
1
+ /**
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.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const crypto = require('crypto');
10
+ const os = require('os');
11
+
12
+ const FREE_LIMIT_PER_DAY = 50;
13
+ const STATE_FILE = path.join(os.homedir(), '.correctover', 'license.json');
14
+
15
+ function loadState() {
16
+ try {
17
+ if (fs.existsSync(STATE_FILE)) {
18
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
19
+ }
20
+ } catch (e) {}
21
+ return { products: {}, license_key: null, installed_at: Date.now() / 1000 };
22
+ }
23
+
24
+ function saveState(state) {
25
+ try {
26
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
27
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
28
+ } catch (e) {}
29
+ }
30
+
31
+ function getProductState(state, product) {
32
+ const today = new Date().toISOString().slice(0, 10);
33
+ if (!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;
39
+ }
40
+ return state.products[product];
41
+ }
42
+
43
+ function verifyLicenseKey(key) {
44
+ if (!key || key.length < 12) return false;
45
+
46
+ // COV-<product>-<hash> (Cloud HMAC)
47
+ if (key.startsWith('COV-')) {
48
+ const parts = key.split('-');
49
+ if (parts.length < 3) return false;
50
+ const productCode = parts.slice(1, -1).join('-');
51
+ const secret = `correctover-${productCode}-2026`;
52
+ const expected = crypto.createHash('sha256').update(secret).digest('hex').slice(0, 12);
53
+ return parts[parts.length - 1].startsWith(expected);
54
+ }
55
+
56
+ // CV-TRL-<base64> / CV-PRO-<base64> (FC XunhuPay)
57
+ if (key.startsWith('CV-')) {
58
+ const parts = key.split('-', 2);
59
+ if (parts.length < 3) return false;
60
+ try {
61
+ let payload = parts[2];
62
+ const dot = payload.indexOf('.');
63
+ if (dot > 0) payload = payload.substring(0, dot);
64
+ const decoded = Buffer.from(payload, 'base64url').toString();
65
+ return decoded.includes('@') || decoded.length > 10;
66
+ } catch (e) {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ return false;
72
+ }
73
+
74
+ function checkLicense(product) {
75
+ const state = loadState();
76
+ const ps = getProductState(state, product);
77
+ const licenseKey = state.license_key || process.env.CORRECTOVER_LICENSE_KEY;
78
+
79
+ if (licenseKey && verifyLicenseKey(licenseKey)) {
80
+ return {
81
+ authorized: true,
82
+ tier: 'pro',
83
+ calls_remaining: Infinity,
84
+ calls_today: ps.calls_today,
85
+ limit: Infinity,
86
+ };
87
+ }
88
+
89
+ const remaining = Math.max(0, FREE_LIMIT_PER_DAY - ps.calls_today);
90
+ return {
91
+ authorized: remaining > 0,
92
+ tier: 'free',
93
+ calls_remaining: remaining,
94
+ calls_today: ps.calls_today,
95
+ limit: FREE_LIMIT_PER_DAY,
96
+ };
97
+ }
98
+
99
+ function recordCall(product) {
100
+ const state = loadState();
101
+ const status = checkLicense(product);
102
+ if (!status.authorized) return status;
103
+
104
+ const ps = getProductState(state, product);
105
+ ps.calls_today += 1;
106
+ ps.total_calls = (ps.total_calls || 0) + 1;
107
+ saveState(state);
108
+
109
+ status.calls_remaining = Math.max(0, status.limit - ps.calls_today);
110
+ status.calls_today = ps.calls_today;
111
+ return status;
112
+ }
113
+
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`;
120
+ }
121
+
122
+ module.exports = {
123
+ FREE_LIMIT_PER_DAY,
124
+ checkLicense,
125
+ recordCall,
126
+ getUpgradeMessage,
127
+ verifyLicenseKey,
128
+ };
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, '&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
+ .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 &amp; 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> &nbsp;·&nbsp; Mode: ${esc(mode)} &nbsp;·&nbsp; correctover-scan v${esc(ver)} (${esc(checksLabel)}) &nbsp;·&nbsp; ${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 &amp; 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 &amp; 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
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Correctover CCS Security Scanner - Core Engine
3
+ * 14 security checks mapped to OWASP AISVS 1.0
4
+ * Shared by CLI / GitHub Action / VS Code Extension
5
+ */
6
+
7
+ const CHECKS = [
8
+ {
9
+ id: 'mcp-tls', category: 'C10 MCP安全', name: 'TLS传输加密', severity: 'critical',
10
+ check: (cfg) => {
11
+ const servers = getServers(cfg);
12
+ if (!servers.length) return 'info';
13
+ return servers.every(s => !s.url || s.url.startsWith('https://') || s.url.startsWith('stdio:')) ? 'pass' : 'fail';
14
+ },
15
+ fix: '所有MCP Server URL应使用HTTPS协议,禁止明文HTTP传输',
16
+ aisvs: 'C10.1'
17
+ },
18
+ {
19
+ id: 'mcp-auth', category: 'C10 MCP安全', name: '服务器鉴权配置', severity: 'high',
20
+ check: (cfg) => {
21
+ const servers = getServers(cfg);
22
+ if (!servers.length) return 'info';
23
+ const hasAuth = servers.some(s => s.headers?.authorization || s.headers?.Authorization || s.env?.API_KEY);
24
+ return hasAuth ? 'pass' : 'warn';
25
+ },
26
+ fix: '为MCP Server配置认证头(Authorization/API Key),防止未授权访问',
27
+ aisvs: 'C10.2'
28
+ },
29
+ {
30
+ id: 'mcp-timeout', category: 'C9 Agent安全', name: '超时配置', severity: 'medium',
31
+ check: (cfg) => {
32
+ return JSON.stringify(cfg).includes('timeout') ? 'pass' : 'warn';
33
+ },
34
+ fix: '为MCP Server连接设置超时时间,防止Agent因无响应Server而挂起',
35
+ aisvs: 'C9.1'
36
+ },
37
+ {
38
+ id: 'cred-exposure', category: 'C5 访问控制', name: '凭证明文暴露', severity: 'critical',
39
+ check: (cfg) => {
40
+ const str = JSON.stringify(cfg);
41
+ const patterns = [/sk-[a-zA-Z0-9]{20,}/, /AKIA[A-Z0-9]{16}/, /ghp_[a-zA-Z0-9]{36}/, /password\s*:\s*["'][^"']+["']/i];
42
+ return patterns.some(p => p.test(str)) ? 'fail' : 'pass';
43
+ },
44
+ fix: '禁止在配置文件中硬编码API密钥。使用环境变量引用(如 ${API_KEY})或密钥管理服务',
45
+ aisvs: 'C5.1'
46
+ },
47
+ {
48
+ id: 'allowed-tools', category: 'C9 Agent安全', name: '工具白名单', severity: 'high',
49
+ check: (cfg) => {
50
+ const str = JSON.stringify(cfg);
51
+ return (str.includes('allowed_tools') || str.includes('allowedTools') || str.includes('permissions')) ? 'pass' : 'warn';
52
+ },
53
+ fix: '配置allowed_tools白名单,限制Agent可调用的工具范围,遵循最小权限原则',
54
+ aisvs: 'C9.3'
55
+ },
56
+ {
57
+ id: 'budget-limit', category: 'C9 Agent安全', name: 'Token预算控制', severity: 'high',
58
+ check: (cfg) => {
59
+ const str = JSON.stringify(cfg);
60
+ return (str.includes('budget') || str.includes('max_tokens') || str.includes('token_limit') || str.includes('cost_limit')) ? 'pass' : 'warn';
61
+ },
62
+ fix: '设置Token消耗预算上限,防止Agent因循环调用导致成本失控',
63
+ aisvs: 'C9.1'
64
+ },
65
+ {
66
+ id: 'ssrf-protection', category: 'C10 MCP安全', name: 'SSRF防护', severity: 'critical',
67
+ check: (cfg) => {
68
+ const servers = getServers(cfg);
69
+ const urls = servers.map(s => s.url || '').join(' ');
70
+ const internal = /169\.254\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|localhost|127\.0\.0\.1/i;
71
+ return internal.test(urls) ? 'fail' : 'pass';
72
+ },
73
+ fix: 'MCP Server URL不应指向内网地址(169.254.x.x/10.x/172.16-31.x/192.168.x),防止SSRF攻击',
74
+ aisvs: 'C10.3'
75
+ },
76
+ {
77
+ id: 'logging', category: 'C12 监控', name: '审计日志配置', severity: 'medium',
78
+ check: (cfg) => {
79
+ const str = JSON.stringify(cfg);
80
+ return (str.includes('log') || str.includes('audit') || str.includes('trace') || str.includes('telemetry')) ? 'pass' : 'warn';
81
+ },
82
+ fix: '启用审计日志记录所有Agent操作,便于事后追溯和安全分析',
83
+ aisvs: 'C12.1'
84
+ },
85
+ {
86
+ id: 'sandbox', category: 'C4 基础设施', name: '沙箱隔离配置', severity: 'medium',
87
+ check: (cfg) => {
88
+ const str = JSON.stringify(cfg);
89
+ return (str.includes('sandbox') || str.includes('isolation') || str.includes('container') || str.includes('docker')) ? 'pass' : 'info';
90
+ },
91
+ fix: '考虑为MCP Server配置沙箱执行环境,限制文件系统和网络访问',
92
+ aisvs: 'C4.1'
93
+ },
94
+ {
95
+ id: 'version-pin', category: 'C6 供应链', name: '依赖版本锁定', severity: 'medium',
96
+ check: (cfg) => {
97
+ const servers = getServers(cfg);
98
+ if (!servers.length) return 'info';
99
+ return servers.some(s => s.command || s.version) ? 'pass' : 'warn';
100
+ },
101
+ fix: '锁定MCP Server依赖的具体版本号,防止供应链攻击',
102
+ aisvs: 'C6.1'
103
+ },
104
+ {
105
+ id: 'error-handling', category: 'C12 监控', name: '错误处理策略', severity: 'medium',
106
+ check: (cfg) => {
107
+ const str = JSON.stringify(cfg);
108
+ return (str.includes('retry') || str.includes('fallback') || str.includes('error') || str.includes('on_error')) ? 'pass' : 'warn';
109
+ },
110
+ fix: '配置错误处理和故障转移策略,确保Agent在Server故障时优雅降级',
111
+ aisvs: 'C12.2'
112
+ },
113
+ {
114
+ id: 'input-validation', category: 'C2 输入验证', name: '输入校验规则', severity: 'high',
115
+ check: (cfg) => {
116
+ const str = JSON.stringify(cfg);
117
+ return (str.includes('validation') || str.includes('schema') || str.includes('input_check') || str.includes('sanitize')) ? 'pass' : 'warn';
118
+ },
119
+ fix: '为Agent输入配置校验规则,防御提示注入和编码走私攻击',
120
+ aisvs: 'C2.1'
121
+ },
122
+ {
123
+ id: 'output-validation', category: 'C7 输出控制', name: '输出校验规则', severity: 'medium',
124
+ check: (cfg) => {
125
+ const str = JSON.stringify(cfg);
126
+ return (str.includes('output') || str.includes('response_check') || str.includes('filter')) ? 'pass' : 'info';
127
+ },
128
+ fix: '对Agent输出进行格式校验和敏感信息过滤',
129
+ aisvs: 'C7.1'
130
+ },
131
+ {
132
+ id: 'kill-switch', category: 'C9 Agent安全', name: '紧急终止机制', severity: 'high',
133
+ check: (cfg) => {
134
+ const str = JSON.stringify(cfg);
135
+ return (str.includes('kill') || str.includes('circuit_breaker') || str.includes('emergency') || str.includes('abort')) ? 'pass' : 'warn';
136
+ },
137
+ fix: '配置紧急终止开关(kill switch),在检测到异常行为时立即停止Agent',
138
+ aisvs: 'C9.5'
139
+ }
140
+ ];
141
+
142
+ function getServers(cfg) {
143
+ if (cfg.mcpServers) return Object.values(cfg.mcpServers);
144
+ if (cfg.servers) return cfg.servers;
145
+ if (Array.isArray(cfg)) return cfg;
146
+ return [];
147
+ }
148
+
149
+ /**
150
+ * Run security scan on a parsed config object
151
+ * @param {Object} config - Parsed MCP config
152
+ * @returns {{ results: Array, stats: Object }}
153
+ */
154
+ function runScan(config) {
155
+ const results = CHECKS.map(check => {
156
+ let status;
157
+ try { status = check.check(config); } catch (e) { status = 'info'; }
158
+ return { ...check, status };
159
+ });
160
+
161
+ const pass = results.filter(r => r.status === 'pass').length;
162
+ const warn = results.filter(r => r.status === 'warn').length;
163
+ const fail = results.filter(r => r.status === 'fail').length;
164
+ const info = results.filter(r => r.status === 'info').length;
165
+ const total = results.length;
166
+ const score = Math.round(((pass * 10 + warn * 5 + info * 7) / (total * 10)) * 100);
167
+
168
+ return { results, stats: { pass, warn, fail, info, total, score } };
169
+ }
170
+
171
+ /**
172
+ * Parse config content (JSON or simple YAML)
173
+ * @param {string} content - File content
174
+ * @param {string} filename - Original filename
175
+ * @returns {Object} Parsed config
176
+ */
177
+ function parseConfig(content, filename = '') {
178
+ if (filename.endsWith('.json') || filename.endsWith('.toml')) {
179
+ return JSON.parse(content);
180
+ }
181
+ // Try JSON first for unknown extensions
182
+ try { return JSON.parse(content); } catch (e) {}
183
+ // Basic YAML parse
184
+ return parseSimpleYAML(content);
185
+ }
186
+
187
+ function parseSimpleYAML(text) {
188
+ const result = {};
189
+ const lines = text.split('\n');
190
+ let currentPath = [];
191
+ let indentStack = [-1];
192
+ for (const line of lines) {
193
+ if (!line.trim() || line.trim().startsWith('#')) continue;
194
+ const indent = line.search(/\S/);
195
+ const match = line.trim().match(/^([^:]+):\s*(.*)/);
196
+ if (!match) continue;
197
+ const key = match[1].trim();
198
+ let val = match[2].trim();
199
+ while (indentStack.length > 1 && indent <= indentStack[indentStack.length - 1]) {
200
+ indentStack.pop();
201
+ currentPath.pop();
202
+ }
203
+ if (val === '' || val === '{}' || val === '[]') {
204
+ currentPath.push(key);
205
+ indentStack.push(indent);
206
+ } else {
207
+ val = val.replace(/^["']|["']$/g, '');
208
+ setNestedValue(result, [...currentPath, key], val);
209
+ }
210
+ }
211
+ return result;
212
+ }
213
+
214
+ function setNestedValue(obj, path, value) {
215
+ let current = obj;
216
+ for (let i = 0; i < path.length - 1; i++) {
217
+ if (!current[path[i]]) current[path[i]] = {};
218
+ current = current[path[i]];
219
+ }
220
+ current[path[path.length - 1]] = value;
221
+ }
222
+
223
+ // Auto-detect MCP config files in a directory
224
+ const KNOWN_CONFIG_PATHS = [
225
+ '.cursor/mcp.json',
226
+ 'claude_desktop_config.json',
227
+ '.claude/mcp.json',
228
+ 'mcp.json',
229
+ 'mcp.yaml',
230
+ 'mcp.yml',
231
+ '.vscode/mcp.json',
232
+ 'config/mcp.json',
233
+ '.mcp/mcp.json',
234
+ 'mcp_config.json',
235
+ ];
236
+
237
+ if (typeof module !== 'undefined' && module.exports) {
238
+ module.exports = { CHECKS, runScan, parseConfig, getServers, KNOWN_CONFIG_PATHS };
239
+ }