shellward 0.7.9 → 0.7.10

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.
@@ -3,6 +3,8 @@ import type { ProjectScanResult } from './project-scan.js';
3
3
  export interface HtmlReportMeta {
4
4
  /** 扫描的项目根 */
5
5
  root: string;
6
+ /** 可选:web 场景下的"返回/再扫一个"链接(CLI 导出时不传) */
7
+ backLink?: string;
6
8
  }
7
9
  /** 生成自包含 HTML 合规报告 */
8
10
  export declare function renderHtmlReport(report: ComplianceReport, scan: ProjectScanResult, locale: 'zh' | 'en', meta: HtmlReportMeta): string;
@@ -171,6 +171,7 @@ export function renderHtmlReport(report, scan, locale, meta) {
171
171
  <style>${CSS}</style>
172
172
  </head>
173
173
  <body>
174
+ ${meta.backLink ? `<div class="backbar"><a href="${esc(meta.backLink)}">← ${t('返回 / 再扫一个', 'Back / scan another')}</a></div>` : ''}
174
175
  <main>
175
176
  <header>
176
177
  <div class="brand">🛡️ Shell<span>Ward</span> <em>${t('合规网关', 'Compliance Gateway')}</em></div>
@@ -224,6 +225,10 @@ const CSS = `
224
225
  body{margin:0;background:var(--bg);color:var(--ink);
225
226
  font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
226
227
  -webkit-font-smoothing:antialiased}
228
+ .backbar{position:sticky;top:0;z-index:9;background:#0f172a;padding:10px 24px}
229
+ .backbar a{color:#fff;font-weight:600;font-size:14px;text-decoration:none}
230
+ .backbar a:hover{color:#fca5a5}
231
+ @media print{.backbar{display:none}}
227
232
  main{max-width:880px;margin:28px auto;background:var(--card);border-radius:16px;
228
233
  box-shadow:0 1px 3px rgba(15,23,42,.06),0 12px 32px rgba(15,23,42,.07);overflow:hidden}
229
234
  header{padding:30px 36px 22px;background:linear-gradient(180deg,#fafbfd,#fff);border-bottom:1px solid var(--line)}
@@ -110,7 +110,7 @@ async function handleRepo(res, repo, locale, inc, dec) {
110
110
  try {
111
111
  await cloneRepo(v.url, dir);
112
112
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir);
113
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }));
113
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url, backLink: '/' }));
114
114
  }
115
115
  catch (e) {
116
116
  const msg = esc(e?.message || String(e));
@@ -133,7 +133,7 @@ async function handleLocal(res, path, locale, inc, dec) {
133
133
  inc();
134
134
  try {
135
135
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, root);
136
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root }));
136
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root, backLink: '/' }));
137
137
  }
138
138
  finally {
139
139
  dec();
@@ -192,7 +192,7 @@ async function handleUpload(req, res, locale, inc, dec) {
192
192
  }
193
193
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir);
194
194
  const rootName = typeof payload.root === 'string' && payload.root ? payload.root : '(uploaded folder)';
195
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: rootName }));
195
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: rootName, backLink: '/' }));
196
196
  }
197
197
  catch (e) {
198
198
  send(res, 500, 'text/html', errorPage('扫描失败:' + esc(e?.message || String(e))));
@@ -248,7 +248,7 @@ function handleDemo(res, locale, inc, dec) {
248
248
  writeFileSync(join(dir, 'data', 'customers.csv'), 'name,id_card,phone,card\n张三,110101199003071233,13800138000,4111111111111111\n');
249
249
  writeFileSync(join(dir, '.env'), 'AWS_ACCESS_KEY=AKIARZ9MKP2QWLS7YV3N\nDB_PASSWORD=Sup3rS3cretProdPwd2026\n');
250
250
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir);
251
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: '示例项目(含风险)/ demo-ai-app' }));
251
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: '示例项目(含风险)/ demo-ai-app', backLink: '/' }));
252
252
  }
253
253
  catch (e) {
254
254
  send(res, 500, 'text/html', errorPage('演示失败:' + esc(e?.message || String(e))));
@@ -285,32 +285,42 @@ function send(res, code, type, body) {
285
285
  function formPage(local) {
286
286
  const urlForm = `
287
287
  <form action="/scan" method="get" onsubmit="var b=this.querySelector('button');b.disabled=true;b.textContent='扫描中…(大仓库需 10–60 秒,请勿重复点击)';">
288
- <label>${local ? ' ' : ''}公开仓库地址</label>
288
+ <label>${local ? ' ' : ''}公开仓库地址</label>
289
289
  <input name="repo" placeholder="https://github.com/owner/repo"${local ? '' : ' autofocus'}>
290
290
  <button type="submit">${local ? '体检该仓库 →' : '开始体检 →'}</button>
291
- <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时改用上方「选择文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
291
+ <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时用上方「上传文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
292
292
  </form>`;
293
- const uploadForm = local ? `
294
- <label>① 在本机点选项目文件夹(推荐 · 零上传)</label>
295
- <div class="browser">
296
- <div class="bpath" id="curpath">加载中…</div>
297
- <ul class="dirs" id="dirs"></ul>
298
- </div>
299
- <button id="scanbtn" type="button">✅ 扫描当前文件夹 →</button>
300
- <p class="hint">📂 在你电脑上点进项目目录,再点"扫描当前文件夹"。<b>服务端直接读取本机文件、零上传、不出本机</b>,自动跳过 node_modules,无需选 3 万个文件。</p>
293
+ // 本地模式:① 上传文件夹(一次选定,最方便) ② 点选文件夹(零上传) ③ URL
294
+ const localForms = local ? `
295
+ <form id="dirform">
296
+ <label>① 上传项目文件夹(最方便)</label>
297
+ <input type="file" id="dir" webkitdirectory directory multiple>
298
+ <button id="dbtn" type="submit">开始体检 →</button>
299
+ <div id="status" class="status"></div>
300
+ <p class="hint">选你的项目文件夹即可。<b>浏览器可能提示"上传 N 个文件"——放心:实际只发送源码/配置(自动跳过 node_modules、图片、超大文件),且只到<u>本机的本地服务</u>,不出本机。</b></p>
301
+ </form>
302
+ <details class="alt"><summary>不想上传?点选文件夹(零上传)</summary>
303
+ <label>② 在本机点选项目文件夹(零上传)</label>
304
+ <div class="browser">
305
+ <div class="bpath" id="curpath">加载中…</div>
306
+ <ul class="dirs" id="dirs"></ul>
307
+ </div>
308
+ <button id="scanbtn" type="button">✅ 扫描当前文件夹 →</button>
309
+ <p class="hint">服务端直接读取本机文件、零上传、不出本机,自动跳过 node_modules。</p>
310
+ </details>
301
311
  <div class="or">— 或 —</div>` : '';
302
312
  return page('ShellWard 合规体检', `
303
313
  <div class="hero">
304
314
  <div class="logo">🛡️ Shell<span>Ward</span> 合规网关</div>
305
315
  <h1>AI 应用合规体检</h1>
306
- <p class="sub">${local ? '选项目文件夹或贴公开仓库链接' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
307
- ${uploadForm}
316
+ <p class="sub">${local ? '上传/点选项目文件夹,或贴公开仓库链接' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
317
+ ${localForms}
308
318
  ${urlForm}
309
319
  <p class="demo">🤔 觉得"秒出"不真? <a href="/demo">▶ 看一个含风险的示例报告</a>(同样秒出,但满屏发现 + 行号)</p>
310
320
  <p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
311
321
  <a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
312
322
  </div>
313
- ${local ? BROWSE_SCRIPT : ''}`);
323
+ ${local ? UPLOAD_SCRIPT + BROWSE_SCRIPT : ''}`);
314
324
  }
315
325
  // 本地目录浏览器:点选文件夹 → 服务端直接扫(零上传,不读 node_modules)
316
326
  const BROWSE_SCRIPT = `<script>
@@ -400,6 +410,9 @@ form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6
400
410
  .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
401
411
  color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
402
412
  .demo{margin:18px 0 0;font-size:13px;color:#475569}.demo a{font-weight:600}
413
+ details.alt{margin:6px 0 10px;text-align:left}
414
+ details.alt summary{cursor:pointer;color:#cb0000;font-size:13px;font-weight:600;padding:6px 0}
415
+ details.alt[open] summary{margin-bottom:8px}
403
416
  .browser{border:1px solid #cbd5e1;border-radius:10px;overflow:hidden;margin:4px 0 10px;text-align:left}
404
417
  .bpath{background:#0f172a;color:#93c5fd;font-family:ui-monospace,Menlo,monospace;font-size:12px;
405
418
  padding:9px 12px;word-break:break-all}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.7.9",
3
+ "version": "0.7.10",
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": [
@@ -43,6 +43,8 @@ const SEV: Record<string, { zh: string; en: string }> = {
43
43
  export interface HtmlReportMeta {
44
44
  /** 扫描的项目根 */
45
45
  root: string
46
+ /** 可选:web 场景下的"返回/再扫一个"链接(CLI 导出时不传) */
47
+ backLink?: string
46
48
  }
47
49
 
48
50
  /** 生成自包含 HTML 合规报告 */
@@ -203,6 +205,7 @@ export function renderHtmlReport(
203
205
  <style>${CSS}</style>
204
206
  </head>
205
207
  <body>
208
+ ${meta.backLink ? `<div class="backbar"><a href="${esc(meta.backLink)}">← ${t('返回 / 再扫一个', 'Back / scan another')}</a></div>` : ''}
206
209
  <main>
207
210
  <header>
208
211
  <div class="brand">🛡️ Shell<span>Ward</span> <em>${t('合规网关', 'Compliance Gateway')}</em></div>
@@ -263,6 +266,10 @@ const CSS = `
263
266
  body{margin:0;background:var(--bg);color:var(--ink);
264
267
  font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
265
268
  -webkit-font-smoothing:antialiased}
269
+ .backbar{position:sticky;top:0;z-index:9;background:#0f172a;padding:10px 24px}
270
+ .backbar a{color:#fff;font-weight:600;font-size:14px;text-decoration:none}
271
+ .backbar a:hover{color:#fca5a5}
272
+ @media print{.backbar{display:none}}
266
273
  main{max-width:880px;margin:28px auto;background:var(--card);border-radius:16px;
267
274
  box-shadow:0 1px 3px rgba(15,23,42,.06),0 12px 32px rgba(15,23,42,.07);overflow:hidden}
268
275
  header{padding:30px 36px 22px;background:linear-gradient(180deg,#fafbfd,#fff);border-bottom:1px solid var(--line)}
@@ -113,7 +113,7 @@ async function handleRepo(res: any, repo: string, locale: 'zh' | 'en', inc: () =
113
113
  try {
114
114
  await cloneRepo(v.url, dir)
115
115
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
116
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }))
116
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url, backLink: '/' }))
117
117
  } catch (e: any) {
118
118
  const msg = esc(e?.message || String(e))
119
119
  send(res, 502, 'text/html', errorPage(
@@ -133,7 +133,7 @@ async function handleLocal(res: any, path: string, locale: 'zh' | 'en', inc: ()
133
133
  inc()
134
134
  try {
135
135
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, root)
136
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root }))
136
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root, backLink: '/' }))
137
137
  } finally {
138
138
  dec()
139
139
  }
@@ -176,7 +176,7 @@ async function handleUpload(req: any, res: any, locale: 'zh' | 'en', inc: () =>
176
176
  }
177
177
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
178
178
  const rootName = typeof payload.root === 'string' && payload.root ? payload.root : '(uploaded folder)'
179
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: rootName }))
179
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: rootName, backLink: '/' }))
180
180
  } catch (e: any) {
181
181
  send(res, 500, 'text/html', errorPage('扫描失败:' + esc(e?.message || String(e))))
182
182
  } finally {
@@ -226,7 +226,7 @@ function handleDemo(res: any, locale: 'zh' | 'en', inc: () => void, dec: () => v
226
226
  writeFileSync(join(dir, '.env'),
227
227
  'AWS_ACCESS_KEY=AKIARZ9MKP2QWLS7YV3N\nDB_PASSWORD=Sup3rS3cretProdPwd2026\n')
228
228
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
229
- send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: '示例项目(含风险)/ demo-ai-app' }))
229
+ send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: '示例项目(含风险)/ demo-ai-app', backLink: '/' }))
230
230
  } catch (e: any) {
231
231
  send(res, 500, 'text/html', errorPage('演示失败:' + esc(e?.message || String(e))))
232
232
  } finally {
@@ -261,34 +261,44 @@ function send(res: any, code: number, type: string, body: string) {
261
261
  function formPage(local: boolean): string {
262
262
  const urlForm = `
263
263
  <form action="/scan" method="get" onsubmit="var b=this.querySelector('button');b.disabled=true;b.textContent='扫描中…(大仓库需 10–60 秒,请勿重复点击)';">
264
- <label>${local ? ' ' : ''}公开仓库地址</label>
264
+ <label>${local ? ' ' : ''}公开仓库地址</label>
265
265
  <input name="repo" placeholder="https://github.com/owner/repo"${local ? '' : ' autofocus'}>
266
266
  <button type="submit">${local ? '体检该仓库 →' : '开始体检 →'}</button>
267
- <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时改用上方「选择文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
267
+ <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时用上方「上传文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
268
268
  </form>`
269
269
 
270
- const uploadForm = local ? `
271
- <label>① 在本机点选项目文件夹(推荐 · 零上传)</label>
272
- <div class="browser">
273
- <div class="bpath" id="curpath">加载中…</div>
274
- <ul class="dirs" id="dirs"></ul>
275
- </div>
276
- <button id="scanbtn" type="button">✅ 扫描当前文件夹 →</button>
277
- <p class="hint">📂 在你电脑上点进项目目录,再点"扫描当前文件夹"。<b>服务端直接读取本机文件、零上传、不出本机</b>,自动跳过 node_modules,无需选 3 万个文件。</p>
270
+ // 本地模式:① 上传文件夹(一次选定,最方便) ② 点选文件夹(零上传) ③ URL
271
+ const localForms = local ? `
272
+ <form id="dirform">
273
+ <label>① 上传项目文件夹(最方便)</label>
274
+ <input type="file" id="dir" webkitdirectory directory multiple>
275
+ <button id="dbtn" type="submit">开始体检 →</button>
276
+ <div id="status" class="status"></div>
277
+ <p class="hint">选你的项目文件夹即可。<b>浏览器可能提示"上传 N 个文件"——放心:实际只发送源码/配置(自动跳过 node_modules、图片、超大文件),且只到<u>本机的本地服务</u>,不出本机。</b></p>
278
+ </form>
279
+ <details class="alt"><summary>不想上传?点选文件夹(零上传)</summary>
280
+ <label>② 在本机点选项目文件夹(零上传)</label>
281
+ <div class="browser">
282
+ <div class="bpath" id="curpath">加载中…</div>
283
+ <ul class="dirs" id="dirs"></ul>
284
+ </div>
285
+ <button id="scanbtn" type="button">✅ 扫描当前文件夹 →</button>
286
+ <p class="hint">服务端直接读取本机文件、零上传、不出本机,自动跳过 node_modules。</p>
287
+ </details>
278
288
  <div class="or">— 或 —</div>` : ''
279
289
 
280
290
  return page('ShellWard 合规体检', `
281
291
  <div class="hero">
282
292
  <div class="logo">🛡️ Shell<span>Ward</span> 合规网关</div>
283
293
  <h1>AI 应用合规体检</h1>
284
- <p class="sub">${local ? '选项目文件夹或贴公开仓库链接' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
285
- ${uploadForm}
294
+ <p class="sub">${local ? '上传/点选项目文件夹,或贴公开仓库链接' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
295
+ ${localForms}
286
296
  ${urlForm}
287
297
  <p class="demo">🤔 觉得"秒出"不真? <a href="/demo">▶ 看一个含风险的示例报告</a>(同样秒出,但满屏发现 + 行号)</p>
288
298
  <p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
289
299
  <a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
290
300
  </div>
291
- ${local ? BROWSE_SCRIPT : ''}`)
301
+ ${local ? UPLOAD_SCRIPT + BROWSE_SCRIPT : ''}`)
292
302
  }
293
303
 
294
304
  // 本地目录浏览器:点选文件夹 → 服务端直接扫(零上传,不读 node_modules)
@@ -382,6 +392,9 @@ form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6
382
392
  .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
383
393
  color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
384
394
  .demo{margin:18px 0 0;font-size:13px;color:#475569}.demo a{font-weight:600}
395
+ details.alt{margin:6px 0 10px;text-align:left}
396
+ details.alt summary{cursor:pointer;color:#cb0000;font-size:13px;font-weight:600;padding:6px 0}
397
+ details.alt[open] summary{margin-bottom:8px}
385
398
  .browser{border:1px solid #cbd5e1;border-radius:10px;overflow:hidden;margin:4px 0 10px;text-align:left}
386
399
  .bpath{background:#0f172a;color:#93c5fd;font-family:ui-monospace,Menlo,monospace;font-size:12px;
387
400
  padding:9px 12px;word-break:break-all}