shellward 0.7.4 → 0.7.6

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 CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/shellward?color=cb0000&label=npm)](https://www.npmjs.com/package/shellward)
10
10
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
11
- [![tests](https://img.shields.io/badge/tests-282%20passing-brightgreen)](#performance)
11
+ [![tests](https://img.shields.io/badge/tests-300%20passing-brightgreen)](#performance)
12
12
  [![deps](https://img.shields.io/badge/dependencies-0-brightgreen)](#performance)
13
13
 
14
14
  **🌐 官网: https://jnmetacode.github.io/shellward/**
@@ -74,7 +74,14 @@ export function renderHtmlReport(report, scan, locale, meta) {
74
74
  // ===== 项目实测风险 =====
75
75
  S.push(sectionHead('🔍', t('项目实测风险', 'Project Scan Findings'), t(`已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限)' : ''} · 耗时 ${scan.durationMs ?? '?'}ms · 应用 ${scan.rulesChecked ?? '?'} 条检测规则`, `Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`)));
76
76
  if (scan.findings.length === 0) {
77
- S.push(`<div class="empty">🟢 ${t('未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。', 'No hardcoded secrets, PII, or overseas endpoints found in project files.')}</div>`);
77
+ // 空结果也要展示"检查过程"——逐项列出查了什么、均未命中,证明确实扫了
78
+ S.push(`<div class="empty">🟢 ${t('逐项检查完成,未在可扫描文件中发现风险。', 'All checks passed — no risks found in scannable files.')}</div>`);
79
+ S.push(`<table class="tbl checked"><tbody>
80
+ <tr><td>🌐 ${t('境外大模型端点 + SDK 依赖', 'Overseas LLM endpoints + SDK deps')}</td><td class="muted">${t('OpenAI / Anthropic / Gemini / Cohere… 共 38 个特征', '38 signatures')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
81
+ <tr><td>🔑 ${t('硬编码密钥', 'Hardcoded secrets')}</td><td class="muted">${t('OpenAI/GitHub/AWS key、私钥、JWT、口令、连接串', 'OpenAI/GitHub/AWS/private key/JWT/password/conn-string')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
82
+ <tr><td>🪪 ${t('中文 PII + 国际 PII', 'Chinese + intl PII')}</td><td class="muted">${t('身份证(校验位)/手机号/银行卡(Luhn)/SSN/信用卡', 'CN ID(checksum)/mobile/UnionPay(Luhn)/SSN/credit card')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
83
+ <tr><td>📂 .env ${t('权限', 'permission')}</td><td class="muted">${t('含密钥的 .env 不应组/其他可读', '.env should not be group/other readable')}</td><td class="right ok">✓ ${t('正常', 'OK')}</td></tr>
84
+ </tbody></table>`);
78
85
  }
79
86
  else {
80
87
  S.push('<div class="chips">');
@@ -258,6 +265,8 @@ section,.reg{padding:0 36px}
258
265
  padding:0 8px;border-radius:999px;margin-left:4px;font-weight:600}
259
266
  .empty{margin:8px 36px;padding:16px 18px;background:var(--pass-bg);color:var(--pass);
260
267
  border-radius:10px;font-weight:600;font-size:14px}
268
+ .checked td.ok{color:var(--pass);font-weight:700}
269
+ .checked td:first-child{font-weight:600;white-space:nowrap}
261
270
 
262
271
  /* chips 概览 */
263
272
  .chips{display:flex;flex-wrap:wrap;gap:8px;margin:6px 36px 4px}
@@ -130,7 +130,24 @@ export function renderProjectFindings(scan, locale) {
130
130
  : `> Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`);
131
131
  L.push('');
132
132
  if (scan.findings.length === 0) {
133
- L.push(zh ? '🟢 未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。' : '🟢 No hardcoded secrets, PII exposure, or overseas endpoints found in project files.');
133
+ L.push(zh ? '🟢 逐项检查完成,未在可扫描文件中发现风险:' : '🟢 All checks passed no risks found:');
134
+ L.push('');
135
+ if (zh) {
136
+ L.push('| 检查项 | 覆盖 | 结果 |');
137
+ L.push('|---|---|---|');
138
+ L.push('| 🌐 境外大模型端点 + SDK 依赖 | OpenAI/Anthropic/Gemini… 38 个特征 | ✓ 0 命中 |');
139
+ L.push('| 🔑 硬编码密钥 | OpenAI/GitHub/AWS key、私钥、JWT、口令、连接串 | ✓ 0 命中 |');
140
+ L.push('| 🪪 中文+国际 PII | 身份证(校验位)/手机号/银行卡(Luhn)/SSN/信用卡 | ✓ 0 命中 |');
141
+ L.push('| 📂 .env 权限 | 含密钥的 .env 不应组/其他可读 | ✓ 正常 |');
142
+ }
143
+ else {
144
+ L.push('| Check | Coverage | Result |');
145
+ L.push('|---|---|---|');
146
+ L.push('| 🌐 Overseas endpoints + SDK deps | 38 signatures | ✓ 0 hits |');
147
+ L.push('| 🔑 Hardcoded secrets | OpenAI/GitHub/AWS/private key/JWT/password | ✓ 0 hits |');
148
+ L.push('| 🪪 PII (CN + intl) | CN ID/mobile/UnionPay/SSN/credit card | ✓ 0 hits |');
149
+ L.push('| 📂 .env permission | group/other-readable check | ✓ OK |');
150
+ }
134
151
  L.push('');
135
152
  return L.join('\n');
136
153
  }
@@ -52,6 +52,12 @@ export function startWebServer(opts) {
52
52
  return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试'));
53
53
  return await handleUpload(req, res, locale, () => { active++; }, () => { active--; });
54
54
  }
55
+ // 演示:扫一个内置的「含风险样例项目」——证明"秒出≠没检查"(满屏发现 + 行号)
56
+ if (u.pathname === '/demo') {
57
+ if (active >= MAX_CONCURRENT)
58
+ return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试'));
59
+ return handleDemo(res, locale, () => { active++; }, () => { active--; });
60
+ }
55
61
  if (u.pathname === '/scan') {
56
62
  if (active >= MAX_CONCURRENT) {
57
63
  return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试(并发上限)'));
@@ -193,6 +199,36 @@ async function handleUpload(req, res, locale, inc, dec) {
193
199
  catch { /* ignore */ }
194
200
  }
195
201
  }
202
+ /** 演示:内置「含风险样例项目」扫描,证明检测真在工作 */
203
+ function handleDemo(res, locale, inc, dec) {
204
+ const dir = mkdtempSync(join(tmpdir(), 'sw-demo-'));
205
+ inc();
206
+ try {
207
+ mkdirSync(join(dir, 'src'), { recursive: true });
208
+ mkdirSync(join(dir, 'data'), { recursive: true });
209
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({
210
+ name: 'demo-ai-app', dependencies: { 'openai': '^4.20.0', '@anthropic-ai/sdk': '^0.20.0', 'express': '^4' },
211
+ }, null, 2));
212
+ writeFileSync(join(dir, 'src', 'config.ts'), 'export const LLM = "https://api.openai.com/v1"\n'
213
+ + 'const OPENAI_KEY = "sk-Rz9MkP2qWlS7yV3nD8tB1hC4xJ6pQsTuVwYz0"\n'
214
+ + 'const GITHUB_TOKEN = "ghp_Rz9MkP2qWlS7yV3nD8tB1hC4xJ6pQsTuVwYz"\n'
215
+ + 'export const ADMIN_PHONE = "13912345678"\n');
216
+ writeFileSync(join(dir, 'data', 'customers.csv'), 'name,id_card,phone,card\n张三,110101199003071233,13800138000,4111111111111111\n');
217
+ writeFileSync(join(dir, '.env'), 'AWS_ACCESS_KEY=AKIARZ9MKP2QWLS7YV3N\nDB_PASSWORD=Sup3rS3cretProdPwd2026\n');
218
+ const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir);
219
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: '示例项目(含风险)/ demo-ai-app' }));
220
+ }
221
+ catch (e) {
222
+ send(res, 500, 'text/html', errorPage('演示失败:' + esc(e?.message || String(e))));
223
+ }
224
+ finally {
225
+ dec();
226
+ try {
227
+ rmSync(dir, { recursive: true, force: true });
228
+ }
229
+ catch { /* ignore */ }
230
+ }
231
+ }
196
232
  /** 浅克隆公开仓库到临时目录(不鉴权、超时、不执行任何仓库代码) */
197
233
  function cloneRepo(url, dir) {
198
234
  return new Promise((res, rej) => {
@@ -227,6 +263,7 @@ function formPage(local) {
227
263
  <label>① 选择本地项目文件夹(推荐)</label>
228
264
  <input type="file" id="dir" webkitdirectory directory multiple>
229
265
  <button id="dbtn" type="submit">开始体检 →</button>
266
+ <div id="status" class="status"></div>
230
267
  <p class="hint">📂 直接选你的项目文件夹,无需敲路径。文件仅发送到<b>本机的本地服务</b>处理,<b>不经过任何外部服务器、不出本机</b>。</p>
231
268
  </form>
232
269
  <div class="or">— 或 —</div>` : '';
@@ -237,23 +274,27 @@ function formPage(local) {
237
274
  <p class="sub">${local ? '选项目文件夹或贴公开仓库链接' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
238
275
  ${uploadForm}
239
276
  ${urlForm}
277
+ <p class="demo">🤔 觉得"秒出"不真? <a href="/demo">▶ 看一个含风险的示例报告</a>(同样秒出,但满屏发现 + 行号)</p>
240
278
  <p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
241
279
  <a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
242
280
  </div>
243
281
  ${local ? UPLOAD_SCRIPT : ''}`);
244
282
  }
245
283
  // 客户端:读取所选文件夹 → 过滤(跳过 node_modules 等、仅文本/配置、限大小) → POST 到本机服务
284
+ // 注意:过滤后缀须与服务端 SCAN_EXT 对齐(含 .md),否则 markdown 项目会被全滤光显得"扫不了"。
246
285
  const UPLOAD_SCRIPT = `<script>
247
286
  (function(){
248
287
  var SKIP=/(^|\\/)(node_modules|\\.git|dist|build|\\.next|out|vendor|coverage|\\.venv|venv|__pycache__|target|\\.cache)(\\/|$)/;
249
- var EXT=/\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|java|php|rs|json|yaml|yml|toml|ini|conf|sh|txt|csv)$/i;
288
+ var EXT=/\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|java|php|rs|json|yaml|yml|toml|ini|conf|sh|txt|csv|md|mdx|ipynb|properties|xml|gradle|tf)$/i;
250
289
  var ENV=/(^|\\/)\\.env(\\.|$)/; var DEP=/^(package\\.json|requirements\\.txt|pyproject\\.toml|go\\.mod)$/;
251
290
  var form=document.getElementById('dirform'); if(!form) return;
291
+ var statusEl=document.getElementById('status');
292
+ function s(m){ if(statusEl){statusEl.textContent=m;statusEl.style.display='block';} }
252
293
  form.addEventListener('submit', async function(e){
253
294
  e.preventDefault();
254
295
  var inp=document.getElementById('dir'), btn=document.getElementById('dbtn');
255
- if(!inp.files||!inp.files.length){alert('请先选择项目文件夹');return;}
256
- btn.disabled=true; btn.textContent='读取中…';
296
+ if(!inp.files||!inp.files.length){ s('请先点上方按钮选择项目文件夹'); return; }
297
+ btn.disabled=true;
257
298
  var picked=[], total=0, root='';
258
299
  for(var i=0;i<inp.files.length;i++){
259
300
  var f=inp.files[i], rel=f.webkitRelativePath||f.name; if(!root)root=rel.split('/')[0];
@@ -264,13 +305,17 @@ const UPLOAD_SCRIPT = `<script>
264
305
  if(picked.length>=3000||total>8388608) break;
265
306
  total+=f.size; picked.push(f);
266
307
  }
267
- if(!picked.length){alert('未找到可扫描的源码/配置文件');btn.disabled=false;btn.textContent='开始体检 →';return;}
268
- btn.textContent='扫描中… ('+picked.length+' 个文件)';
308
+ if(!picked.length){ s('未找到可扫描的源码/配置文件(已自动跳过 node_modules、图片、超大文件)。请选含代码或配置的目录。'); btn.disabled=false; return; }
309
+ s('读取 '+picked.length+' 个文件…');
269
310
  var out=[]; for(var j=0;j<picked.length;j++){ try{ out.push({path:picked[j].webkitRelativePath||picked[j].name, content:await picked[j].text()}); }catch(_){} }
311
+ s('扫描中…('+out.length+' 个文件,请稍候)');
270
312
  try{
271
313
  var resp=await fetch('/scan-files',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({root:root,files:out})});
272
- var html=await resp.text(); document.open(); document.write(html); document.close();
273
- }catch(err){ alert('扫描失败: '+err); btn.disabled=false; btn.textContent='开始体检 →'; }
314
+ if(!resp.ok){ s('扫描失败:HTTP '+resp.status+'。请重试,或改用命令行 npx shellward scan。'); btn.disabled=false; return; }
315
+ var html=await resp.text();
316
+ // 用 Blob URL 跳转展示报告(比 document.write 可靠)
317
+ window.location.href=URL.createObjectURL(new Blob([html],{type:'text/html'}));
318
+ }catch(err){ s('扫描失败:'+(err&&err.message||err)+'。请重试。'); btn.disabled=false; }
274
319
  });
275
320
  })();
276
321
  </script>`;
@@ -302,6 +347,9 @@ button{background:#cb0000;color:#fff;border:0;border-radius:10px;padding:14px;fo
302
347
  font-weight:700;cursor:pointer;margin-top:4px}button:hover{background:#a80000}
303
348
  button:disabled{background:#94a3b8;cursor:default}
304
349
  form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6px 0 14px}
350
+ .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
351
+ color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
352
+ .demo{margin:18px 0 0;font-size:13px;color:#475569}.demo a{font-weight:600}
305
353
  .foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
306
354
  .back{font-weight:600}
307
355
  </style></head><body>${body}</body></html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "mcpName": "io.github.jnMetaCode/shellward",
5
5
  "description": "AI agent security & MCP security middleware — prompt injection detection, AI firewall, runtime guardrails & data-loss prevention for LLM tool calls. 8-layer defense against data exfiltration & dangerous commands. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents, Hermes Agent.",
6
6
  "keywords": [
@@ -57,7 +57,7 @@
57
57
  "scripts": {
58
58
  "build": "tsc",
59
59
  "mcp": "npx tsx src/mcp-server.ts",
60
- "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-rugpull.ts && npx tsx test-redos.ts && npx tsx test-mcp-client.ts && npx tsx test-mcp.ts && npx tsx test-compliance.ts",
60
+ "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-rugpull.ts && npx tsx test-redos.ts && npx tsx test-mcp-client.ts && npx tsx test-mcp.ts && npx tsx test-compliance.ts && npx tsx test-web.ts",
61
61
  "test:redos": "npx tsx test-redos.ts",
62
62
  "test:compliance": "npx tsx test-compliance.ts",
63
63
  "test:integration": "npx tsx test-integration.ts",
@@ -100,8 +100,14 @@ export function renderHtmlReport(
100
100
  `Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`)))
101
101
 
102
102
  if (scan.findings.length === 0) {
103
- S.push(`<div class="empty">🟢 ${t('未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。',
104
- 'No hardcoded secrets, PII, or overseas endpoints found in project files.')}</div>`)
103
+ // 空结果也要展示"检查过程"——逐项列出查了什么、均未命中,证明确实扫了
104
+ S.push(`<div class="empty">🟢 ${t('逐项检查完成,未在可扫描文件中发现风险。', 'All checks passed no risks found in scannable files.')}</div>`)
105
+ S.push(`<table class="tbl checked"><tbody>
106
+ <tr><td>🌐 ${t('境外大模型端点 + SDK 依赖', 'Overseas LLM endpoints + SDK deps')}</td><td class="muted">${t('OpenAI / Anthropic / Gemini / Cohere… 共 38 个特征', '38 signatures')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
107
+ <tr><td>🔑 ${t('硬编码密钥', 'Hardcoded secrets')}</td><td class="muted">${t('OpenAI/GitHub/AWS key、私钥、JWT、口令、连接串', 'OpenAI/GitHub/AWS/private key/JWT/password/conn-string')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
108
+ <tr><td>🪪 ${t('中文 PII + 国际 PII', 'Chinese + intl PII')}</td><td class="muted">${t('身份证(校验位)/手机号/银行卡(Luhn)/SSN/信用卡', 'CN ID(checksum)/mobile/UnionPay(Luhn)/SSN/credit card')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
109
+ <tr><td>📂 .env ${t('权限', 'permission')}</td><td class="muted">${t('含密钥的 .env 不应组/其他可读', '.env should not be group/other readable')}</td><td class="right ok">✓ ${t('正常', 'OK')}</td></tr>
110
+ </tbody></table>`)
105
111
  } else {
106
112
  S.push('<div class="chips">')
107
113
  for (const k of KIND_ORDER) if (scan.counts[k] > 0) {
@@ -296,6 +302,8 @@ section,.reg{padding:0 36px}
296
302
  padding:0 8px;border-radius:999px;margin-left:4px;font-weight:600}
297
303
  .empty{margin:8px 36px;padding:16px 18px;background:var(--pass-bg);color:var(--pass);
298
304
  border-radius:10px;font-weight:600;font-size:14px}
305
+ .checked td.ok{color:var(--pass);font-weight:700}
306
+ .checked td:first-child{font-weight:600;white-space:nowrap}
299
307
 
300
308
  /* chips 概览 */
301
309
  .chips{display:flex;flex-wrap:wrap;gap:8px;margin:6px 36px 4px}
@@ -146,7 +146,23 @@ export function renderProjectFindings(scan: ProjectScanResult, locale: 'zh' | 'e
146
146
  L.push('')
147
147
 
148
148
  if (scan.findings.length === 0) {
149
- L.push(zh ? '🟢 未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。' : '🟢 No hardcoded secrets, PII exposure, or overseas endpoints found in project files.')
149
+ L.push(zh ? '🟢 逐项检查完成,未在可扫描文件中发现风险:' : '🟢 All checks passed no risks found:')
150
+ L.push('')
151
+ if (zh) {
152
+ L.push('| 检查项 | 覆盖 | 结果 |')
153
+ L.push('|---|---|---|')
154
+ L.push('| 🌐 境外大模型端点 + SDK 依赖 | OpenAI/Anthropic/Gemini… 38 个特征 | ✓ 0 命中 |')
155
+ L.push('| 🔑 硬编码密钥 | OpenAI/GitHub/AWS key、私钥、JWT、口令、连接串 | ✓ 0 命中 |')
156
+ L.push('| 🪪 中文+国际 PII | 身份证(校验位)/手机号/银行卡(Luhn)/SSN/信用卡 | ✓ 0 命中 |')
157
+ L.push('| 📂 .env 权限 | 含密钥的 .env 不应组/其他可读 | ✓ 正常 |')
158
+ } else {
159
+ L.push('| Check | Coverage | Result |')
160
+ L.push('|---|---|---|')
161
+ L.push('| 🌐 Overseas endpoints + SDK deps | 38 signatures | ✓ 0 hits |')
162
+ L.push('| 🔑 Hardcoded secrets | OpenAI/GitHub/AWS/private key/JWT/password | ✓ 0 hits |')
163
+ L.push('| 🪪 PII (CN + intl) | CN ID/mobile/UnionPay/SSN/credit card | ✓ 0 hits |')
164
+ L.push('| 📂 .env permission | group/other-readable check | ✓ OK |')
165
+ }
150
166
  L.push('')
151
167
  return L.join('\n')
152
168
  }
@@ -58,6 +58,11 @@ export function startWebServer(opts: WebServerOptions): void {
58
58
  if (active >= MAX_CONCURRENT) return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试'))
59
59
  return await handleUpload(req, res, locale, () => { active++ }, () => { active-- })
60
60
  }
61
+ // 演示:扫一个内置的「含风险样例项目」——证明"秒出≠没检查"(满屏发现 + 行号)
62
+ if (u.pathname === '/demo') {
63
+ if (active >= MAX_CONCURRENT) return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试'))
64
+ return handleDemo(res, locale, () => { active++ }, () => { active-- })
65
+ }
61
66
  if (u.pathname === '/scan') {
62
67
  if (active >= MAX_CONCURRENT) {
63
68
  return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试(并发上限)'))
@@ -175,6 +180,35 @@ async function handleUpload(req: any, res: any, locale: 'zh' | 'en', inc: () =>
175
180
  }
176
181
  }
177
182
 
183
+ /** 演示:内置「含风险样例项目」扫描,证明检测真在工作 */
184
+ function handleDemo(res: any, locale: 'zh' | 'en', inc: () => void, dec: () => void) {
185
+ const dir = mkdtempSync(join(tmpdir(), 'sw-demo-'))
186
+ inc()
187
+ try {
188
+ mkdirSync(join(dir, 'src'), { recursive: true })
189
+ mkdirSync(join(dir, 'data'), { recursive: true })
190
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({
191
+ name: 'demo-ai-app', dependencies: { 'openai': '^4.20.0', '@anthropic-ai/sdk': '^0.20.0', 'express': '^4' },
192
+ }, null, 2))
193
+ writeFileSync(join(dir, 'src', 'config.ts'),
194
+ 'export const LLM = "https://api.openai.com/v1"\n'
195
+ + 'const OPENAI_KEY = "sk-Rz9MkP2qWlS7yV3nD8tB1hC4xJ6pQsTuVwYz0"\n'
196
+ + 'const GITHUB_TOKEN = "ghp_Rz9MkP2qWlS7yV3nD8tB1hC4xJ6pQsTuVwYz"\n'
197
+ + 'export const ADMIN_PHONE = "13912345678"\n')
198
+ writeFileSync(join(dir, 'data', 'customers.csv'),
199
+ 'name,id_card,phone,card\n张三,110101199003071233,13800138000,4111111111111111\n')
200
+ writeFileSync(join(dir, '.env'),
201
+ 'AWS_ACCESS_KEY=AKIARZ9MKP2QWLS7YV3N\nDB_PASSWORD=Sup3rS3cretProdPwd2026\n')
202
+ const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
203
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: '示例项目(含风险)/ demo-ai-app' }))
204
+ } catch (e: any) {
205
+ send(res, 500, 'text/html', errorPage('演示失败:' + esc(e?.message || String(e))))
206
+ } finally {
207
+ dec()
208
+ try { rmSync(dir, { recursive: true, force: true }) } catch { /* ignore */ }
209
+ }
210
+ }
211
+
178
212
  /** 浅克隆公开仓库到临时目录(不鉴权、超时、不执行任何仓库代码) */
179
213
  function cloneRepo(url: string, dir: string): Promise<void> {
180
214
  return new Promise((res, rej) => {
@@ -212,6 +246,7 @@ function formPage(local: boolean): string {
212
246
  <label>① 选择本地项目文件夹(推荐)</label>
213
247
  <input type="file" id="dir" webkitdirectory directory multiple>
214
248
  <button id="dbtn" type="submit">开始体检 →</button>
249
+ <div id="status" class="status"></div>
215
250
  <p class="hint">📂 直接选你的项目文件夹,无需敲路径。文件仅发送到<b>本机的本地服务</b>处理,<b>不经过任何外部服务器、不出本机</b>。</p>
216
251
  </form>
217
252
  <div class="or">— 或 —</div>` : ''
@@ -223,6 +258,7 @@ function formPage(local: boolean): string {
223
258
  <p class="sub">${local ? '选项目文件夹或贴公开仓库链接' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
224
259
  ${uploadForm}
225
260
  ${urlForm}
261
+ <p class="demo">🤔 觉得"秒出"不真? <a href="/demo">▶ 看一个含风险的示例报告</a>(同样秒出,但满屏发现 + 行号)</p>
226
262
  <p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
227
263
  <a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
228
264
  </div>
@@ -230,17 +266,20 @@ function formPage(local: boolean): string {
230
266
  }
231
267
 
232
268
  // 客户端:读取所选文件夹 → 过滤(跳过 node_modules 等、仅文本/配置、限大小) → POST 到本机服务
269
+ // 注意:过滤后缀须与服务端 SCAN_EXT 对齐(含 .md),否则 markdown 项目会被全滤光显得"扫不了"。
233
270
  const UPLOAD_SCRIPT = `<script>
234
271
  (function(){
235
272
  var SKIP=/(^|\\/)(node_modules|\\.git|dist|build|\\.next|out|vendor|coverage|\\.venv|venv|__pycache__|target|\\.cache)(\\/|$)/;
236
- var EXT=/\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|java|php|rs|json|yaml|yml|toml|ini|conf|sh|txt|csv)$/i;
273
+ var EXT=/\\.(ts|tsx|js|jsx|mjs|cjs|py|go|rb|java|php|rs|json|yaml|yml|toml|ini|conf|sh|txt|csv|md|mdx|ipynb|properties|xml|gradle|tf)$/i;
237
274
  var ENV=/(^|\\/)\\.env(\\.|$)/; var DEP=/^(package\\.json|requirements\\.txt|pyproject\\.toml|go\\.mod)$/;
238
275
  var form=document.getElementById('dirform'); if(!form) return;
276
+ var statusEl=document.getElementById('status');
277
+ function s(m){ if(statusEl){statusEl.textContent=m;statusEl.style.display='block';} }
239
278
  form.addEventListener('submit', async function(e){
240
279
  e.preventDefault();
241
280
  var inp=document.getElementById('dir'), btn=document.getElementById('dbtn');
242
- if(!inp.files||!inp.files.length){alert('请先选择项目文件夹');return;}
243
- btn.disabled=true; btn.textContent='读取中…';
281
+ if(!inp.files||!inp.files.length){ s('请先点上方按钮选择项目文件夹'); return; }
282
+ btn.disabled=true;
244
283
  var picked=[], total=0, root='';
245
284
  for(var i=0;i<inp.files.length;i++){
246
285
  var f=inp.files[i], rel=f.webkitRelativePath||f.name; if(!root)root=rel.split('/')[0];
@@ -251,13 +290,17 @@ const UPLOAD_SCRIPT = `<script>
251
290
  if(picked.length>=3000||total>8388608) break;
252
291
  total+=f.size; picked.push(f);
253
292
  }
254
- if(!picked.length){alert('未找到可扫描的源码/配置文件');btn.disabled=false;btn.textContent='开始体检 →';return;}
255
- btn.textContent='扫描中… ('+picked.length+' 个文件)';
293
+ if(!picked.length){ s('未找到可扫描的源码/配置文件(已自动跳过 node_modules、图片、超大文件)。请选含代码或配置的目录。'); btn.disabled=false; return; }
294
+ s('读取 '+picked.length+' 个文件…');
256
295
  var out=[]; for(var j=0;j<picked.length;j++){ try{ out.push({path:picked[j].webkitRelativePath||picked[j].name, content:await picked[j].text()}); }catch(_){} }
296
+ s('扫描中…('+out.length+' 个文件,请稍候)');
257
297
  try{
258
298
  var resp=await fetch('/scan-files',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({root:root,files:out})});
259
- var html=await resp.text(); document.open(); document.write(html); document.close();
260
- }catch(err){ alert('扫描失败: '+err); btn.disabled=false; btn.textContent='开始体检 →'; }
299
+ if(!resp.ok){ s('扫描失败:HTTP '+resp.status+'。请重试,或改用命令行 npx shellward scan。'); btn.disabled=false; return; }
300
+ var html=await resp.text();
301
+ // 用 Blob URL 跳转展示报告(比 document.write 可靠)
302
+ window.location.href=URL.createObjectURL(new Blob([html],{type:'text/html'}));
303
+ }catch(err){ s('扫描失败:'+(err&&err.message||err)+'。请重试。'); btn.disabled=false; }
261
304
  });
262
305
  })();
263
306
  </script>`
@@ -291,6 +334,9 @@ button{background:#cb0000;color:#fff;border:0;border-radius:10px;padding:14px;fo
291
334
  font-weight:700;cursor:pointer;margin-top:4px}button:hover{background:#a80000}
292
335
  button:disabled{background:#94a3b8;cursor:default}
293
336
  form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6px 0 14px}
337
+ .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
338
+ color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
339
+ .demo{margin:18px 0 0;font-size:13px;color:#475569}.demo a{font-weight:600}
294
340
  .foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
295
341
  .back{font-weight:600}
296
342
  </style></head><body>${body}</body></html>`