shellward 0.7.7 → 0.7.8

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-300%20passing-brightgreen)](#performance)
11
+ [![tests](https://img.shields.io/badge/tests-302%20passing-brightgreen)](#performance)
12
12
  [![deps](https://img.shields.io/badge/dependencies-0-brightgreen)](#performance)
13
13
 
14
14
  **🌐 官网: https://jnmetacode.github.io/shellward/**
@@ -14,8 +14,8 @@
14
14
  // - 并发上限,防滥用
15
15
  import { createServer } from 'http';
16
16
  import { spawn } from 'child_process';
17
- import { mkdtempSync, rmSync, existsSync, statSync, mkdirSync, writeFileSync } from 'fs';
18
- import { tmpdir } from 'os';
17
+ import { mkdtempSync, rmSync, existsSync, statSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
18
+ import { tmpdir, homedir } from 'os';
19
19
  import { join, resolve, dirname, normalize, isAbsolute } from 'path';
20
20
  import { runProjectComplianceAudit } from '../compliance/audit.js';
21
21
  import { renderHtmlReport } from '../compliance/html-report.js';
@@ -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 === '/browse') {
57
+ if (!opts.local)
58
+ return send(res, 403, 'application/json', JSON.stringify({ error: '仅本地模式可用' }));
59
+ return handleBrowse(res, u.searchParams.get('dir'));
60
+ }
55
61
  // 演示:扫一个内置的「含风险样例项目」——证明"秒出≠没检查"(满屏发现 + 行号)
56
62
  if (u.pathname === '/demo') {
57
63
  if (active >= MAX_CONCURRENT)
@@ -199,6 +205,32 @@ async function handleUpload(req, res, locale, inc, dec) {
199
205
  catch { /* ignore */ }
200
206
  }
201
207
  }
208
+ /** 本地目录浏览:返回某目录下的子目录列表(供网页点选;不读文件内容、不上传) */
209
+ function handleBrowse(res, dirParam) {
210
+ try {
211
+ const abs = resolve(dirParam && dirParam.trim() ? dirParam : homedir());
212
+ const entries = readdirSync(abs, { withFileTypes: true })
213
+ .filter(e => { try {
214
+ return e.isDirectory();
215
+ }
216
+ catch {
217
+ return false;
218
+ } })
219
+ .map(e => e.name)
220
+ .filter(n => !n.startsWith('.') && n !== 'node_modules')
221
+ .sort((a, b) => a.localeCompare(b))
222
+ .slice(0, 500);
223
+ const parent = dirname(abs);
224
+ send(res, 200, 'application/json', JSON.stringify({
225
+ current: abs,
226
+ parent: parent === abs ? null : parent,
227
+ dirs: entries,
228
+ }));
229
+ }
230
+ catch (e) {
231
+ send(res, 200, 'application/json', JSON.stringify({ error: e?.message || String(e) }));
232
+ }
233
+ }
202
234
  /** 演示:内置「含风险样例项目」扫描,证明检测真在工作 */
203
235
  function handleDemo(res, locale, inc, dec) {
204
236
  const dir = mkdtempSync(join(tmpdir(), 'sw-demo-'));
@@ -259,13 +291,13 @@ function formPage(local) {
259
291
  <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时改用上方「选择文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
260
292
  </form>`;
261
293
  const uploadForm = local ? `
262
- <form id="dirform">
263
- <label>① 选择本地项目文件夹(推荐)</label>
264
- <input type="file" id="dir" webkitdirectory directory multiple>
265
- <button id="dbtn" type="submit">开始体检 →</button>
266
- <div id="status" class="status"></div>
267
- <p class="hint">📂 直接选你的项目文件夹,无需敲路径。文件仅发送到<b>本机的本地服务</b>处理,<b>不经过任何外部服务器、不出本机</b>。</p>
268
- </form>
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>
269
301
  <div class="or">— 或 —</div>` : '';
270
302
  return page('ShellWard 合规体检', `
271
303
  <div class="hero">
@@ -278,10 +310,28 @@ function formPage(local) {
278
310
  <p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
279
311
  <a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
280
312
  </div>
281
- ${local ? UPLOAD_SCRIPT : ''}`);
313
+ ${local ? BROWSE_SCRIPT : ''}`);
282
314
  }
283
- // 客户端:读取所选文件夹过滤(跳过 node_modules 等、仅文本/配置、限大小) → POST 到本机服务
284
- // 注意:过滤后缀须与服务端 SCAN_EXT 对齐(含 .md),否则 markdown 项目会被全滤光显得"扫不了"。
315
+ // 本地目录浏览器:点选文件夹服务端直接扫(零上传,不读 node_modules
316
+ const BROWSE_SCRIPT = `<script>
317
+ (function(){
318
+ var cur='';
319
+ function load(dir){
320
+ var cp=document.getElementById('curpath'); if(cp)cp.textContent='加载中…';
321
+ fetch('/browse?dir='+encodeURIComponent(dir||'')).then(function(r){return r.json()}).then(function(d){
322
+ if(d.error){ if(cp)cp.textContent='无法读取:'+d.error; return; }
323
+ cur=d.current; if(cp)cp.textContent=cur;
324
+ var ul=document.getElementById('dirs'); if(!ul)return; ul.innerHTML='';
325
+ if(d.parent){ var up=document.createElement('li'); up.className='up'; up.textContent='⬆ 上级目录'; up.onclick=function(){load(d.parent)}; ul.appendChild(up); }
326
+ if(!d.dirs.length){ var e=document.createElement('li'); e.className='empty'; e.textContent='(此目录无子文件夹,可直接点上方扫描)'; ul.appendChild(e); }
327
+ d.dirs.forEach(function(name){ var li=document.createElement('li'); li.textContent='📁 '+name; li.onclick=function(){ load(cur.replace(/\\/+$/,'')+'/'+name) }; ul.appendChild(li); });
328
+ }).catch(function(e){ if(cp)cp.textContent='错误:'+e; });
329
+ }
330
+ var sb=document.getElementById('scanbtn');
331
+ if(sb){ sb.onclick=function(){ if(cur){ sb.disabled=true; sb.textContent='扫描中…'; window.location.href='/scan?path='+encodeURIComponent(cur); } }; load(''); }
332
+ })();
333
+ </script>`;
334
+ // (旧上传脚本保留备用,当前本地模式改用目录浏览器)
285
335
  const UPLOAD_SCRIPT = `<script>
286
336
  (function(){
287
337
  var SKIP=/(^|\\/)(node_modules|\\.git|dist|build|\\.next|out|vendor|coverage|\\.venv|venv|__pycache__|target|\\.cache)(\\/|$)/;
@@ -350,6 +400,14 @@ form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6
350
400
  .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
351
401
  color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
352
402
  .demo{margin:18px 0 0;font-size:13px;color:#475569}.demo a{font-weight:600}
403
+ .browser{border:1px solid #cbd5e1;border-radius:10px;overflow:hidden;margin:4px 0 10px;text-align:left}
404
+ .bpath{background:#0f172a;color:#93c5fd;font-family:ui-monospace,Menlo,monospace;font-size:12px;
405
+ padding:9px 12px;word-break:break-all}
406
+ .dirs{list-style:none;margin:0;padding:0;max-height:240px;overflow-y:auto}
407
+ .dirs li{padding:9px 14px;border-top:1px solid #eef2f7;cursor:pointer;font-size:14px}
408
+ .dirs li:hover{background:#f1f5f9}
409
+ .dirs li.up{color:#cb0000;font-weight:600}
410
+ .dirs li.empty{color:#94a3b8;cursor:default;font-size:13px}
353
411
  .foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
354
412
  .back{font-weight:600}
355
413
  </style></head><body>${body}</body></html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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": [
@@ -15,8 +15,8 @@
15
15
 
16
16
  import { createServer } from 'http'
17
17
  import { spawn } from 'child_process'
18
- import { mkdtempSync, rmSync, existsSync, statSync, mkdirSync, writeFileSync } from 'fs'
19
- import { tmpdir } from 'os'
18
+ import { mkdtempSync, rmSync, existsSync, statSync, mkdirSync, writeFileSync, readdirSync } from 'fs'
19
+ import { tmpdir, homedir } from 'os'
20
20
  import { join, resolve, dirname, normalize, isAbsolute } from 'path'
21
21
  import { runProjectComplianceAudit } from '../compliance/audit.js'
22
22
  import { renderHtmlReport } from '../compliance/html-report.js'
@@ -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 === '/browse') {
63
+ if (!opts.local) return send(res, 403, 'application/json', JSON.stringify({ error: '仅本地模式可用' }))
64
+ return handleBrowse(res, u.searchParams.get('dir'))
65
+ }
61
66
  // 演示:扫一个内置的「含风险样例项目」——证明"秒出≠没检查"(满屏发现 + 行号)
62
67
  if (u.pathname === '/demo') {
63
68
  if (active >= MAX_CONCURRENT) return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试'))
@@ -180,6 +185,27 @@ async function handleUpload(req: any, res: any, locale: 'zh' | 'en', inc: () =>
180
185
  }
181
186
  }
182
187
 
188
+ /** 本地目录浏览:返回某目录下的子目录列表(供网页点选;不读文件内容、不上传) */
189
+ function handleBrowse(res: any, dirParam: string | null) {
190
+ try {
191
+ const abs = resolve(dirParam && dirParam.trim() ? dirParam : homedir())
192
+ const entries = readdirSync(abs, { withFileTypes: true })
193
+ .filter(e => { try { return e.isDirectory() } catch { return false } })
194
+ .map(e => e.name)
195
+ .filter(n => !n.startsWith('.') && n !== 'node_modules')
196
+ .sort((a, b) => a.localeCompare(b))
197
+ .slice(0, 500)
198
+ const parent = dirname(abs)
199
+ send(res, 200, 'application/json', JSON.stringify({
200
+ current: abs,
201
+ parent: parent === abs ? null : parent,
202
+ dirs: entries,
203
+ }))
204
+ } catch (e: any) {
205
+ send(res, 200, 'application/json', JSON.stringify({ error: e?.message || String(e) }))
206
+ }
207
+ }
208
+
183
209
  /** 演示:内置「含风险样例项目」扫描,证明检测真在工作 */
184
210
  function handleDemo(res: any, locale: 'zh' | 'en', inc: () => void, dec: () => void) {
185
211
  const dir = mkdtempSync(join(tmpdir(), 'sw-demo-'))
@@ -242,13 +268,13 @@ function formPage(local: boolean): string {
242
268
  </form>`
243
269
 
244
270
  const uploadForm = local ? `
245
- <form id="dirform">
246
- <label>① 选择本地项目文件夹(推荐)</label>
247
- <input type="file" id="dir" webkitdirectory directory multiple>
248
- <button id="dbtn" type="submit">开始体检 →</button>
249
- <div id="status" class="status"></div>
250
- <p class="hint">📂 直接选你的项目文件夹,无需敲路径。文件仅发送到<b>本机的本地服务</b>处理,<b>不经过任何外部服务器、不出本机</b>。</p>
251
- </form>
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>
252
278
  <div class="or">— 或 —</div>` : ''
253
279
 
254
280
  return page('ShellWard 合规体检', `
@@ -262,11 +288,30 @@ function formPage(local: boolean): string {
262
288
  <p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
263
289
  <a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
264
290
  </div>
265
- ${local ? UPLOAD_SCRIPT : ''}`)
291
+ ${local ? BROWSE_SCRIPT : ''}`)
266
292
  }
267
293
 
268
- // 客户端:读取所选文件夹过滤(跳过 node_modules 等、仅文本/配置、限大小) → POST 到本机服务
269
- // 注意:过滤后缀须与服务端 SCAN_EXT 对齐(含 .md),否则 markdown 项目会被全滤光显得"扫不了"。
294
+ // 本地目录浏览器:点选文件夹服务端直接扫(零上传,不读 node_modules
295
+ const BROWSE_SCRIPT = `<script>
296
+ (function(){
297
+ var cur='';
298
+ function load(dir){
299
+ var cp=document.getElementById('curpath'); if(cp)cp.textContent='加载中…';
300
+ fetch('/browse?dir='+encodeURIComponent(dir||'')).then(function(r){return r.json()}).then(function(d){
301
+ if(d.error){ if(cp)cp.textContent='无法读取:'+d.error; return; }
302
+ cur=d.current; if(cp)cp.textContent=cur;
303
+ var ul=document.getElementById('dirs'); if(!ul)return; ul.innerHTML='';
304
+ if(d.parent){ var up=document.createElement('li'); up.className='up'; up.textContent='⬆ 上级目录'; up.onclick=function(){load(d.parent)}; ul.appendChild(up); }
305
+ if(!d.dirs.length){ var e=document.createElement('li'); e.className='empty'; e.textContent='(此目录无子文件夹,可直接点上方扫描)'; ul.appendChild(e); }
306
+ d.dirs.forEach(function(name){ var li=document.createElement('li'); li.textContent='📁 '+name; li.onclick=function(){ load(cur.replace(/\\/+$/,'')+'/'+name) }; ul.appendChild(li); });
307
+ }).catch(function(e){ if(cp)cp.textContent='错误:'+e; });
308
+ }
309
+ var sb=document.getElementById('scanbtn');
310
+ if(sb){ sb.onclick=function(){ if(cur){ sb.disabled=true; sb.textContent='扫描中…'; window.location.href='/scan?path='+encodeURIComponent(cur); } }; load(''); }
311
+ })();
312
+ </script>`
313
+
314
+ // (旧上传脚本保留备用,当前本地模式改用目录浏览器)
270
315
  const UPLOAD_SCRIPT = `<script>
271
316
  (function(){
272
317
  var SKIP=/(^|\\/)(node_modules|\\.git|dist|build|\\.next|out|vendor|coverage|\\.venv|venv|__pycache__|target|\\.cache)(\\/|$)/;
@@ -337,6 +382,14 @@ form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6
337
382
  .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
338
383
  color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
339
384
  .demo{margin:18px 0 0;font-size:13px;color:#475569}.demo a{font-weight:600}
385
+ .browser{border:1px solid #cbd5e1;border-radius:10px;overflow:hidden;margin:4px 0 10px;text-align:left}
386
+ .bpath{background:#0f172a;color:#93c5fd;font-family:ui-monospace,Menlo,monospace;font-size:12px;
387
+ padding:9px 12px;word-break:break-all}
388
+ .dirs{list-style:none;margin:0;padding:0;max-height:240px;overflow-y:auto}
389
+ .dirs li{padding:9px 14px;border-top:1px solid #eef2f7;cursor:pointer;font-size:14px}
390
+ .dirs li:hover{background:#f1f5f9}
391
+ .dirs li.up{color:#cb0000;font-weight:600}
392
+ .dirs li.empty{color:#94a3b8;cursor:default;font-size:13px}
340
393
  .foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
341
394
  .back{font-weight:600}
342
395
  </style></head><body>${body}</body></html>`