shellward 0.7.3 → 0.7.5

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/**
@@ -390,6 +390,9 @@ Effectiveness is measured, not asserted. `npm run bench` runs every detector ove
390
390
  | Dangerous commands | 100% | 100% | 100% |
391
391
  | PII / secrets | 100% | 100% | 100% |
392
392
  | MCP tool poisoning | 100% | 100% | 100% |
393
+ | **Compliance scan** (overseas / secret / PII vs hard negatives) | 100% | 100% | 100% |
394
+
395
+ The compliance scanner has its own gated corpus — `npm run bench:scan` runs the **real `scanProject` pipeline** over 31 labeled cases (17 real risks + 14 hard negatives: domestic endpoints, placeholder keys, doc examples, lock files, invalid checksums). Self-authored corpus, CI-gated against regression.
393
396
 
394
397
  83 gated samples (attacks + hard negatives). Zero-width-interleaved and empty-quote (`r''m`) obfuscation are normalized before matching. The corpus also tracks **5 documented bypasses** (leetspeak, base64, non-zh/en languages, shell variable indirection) that regex/heuristics are not expected to catch — listed explicitly and excluded from the gate rather than hidden.
395
398
 
@@ -227,6 +227,7 @@ function formPage(local) {
227
227
  <label>① 选择本地项目文件夹(推荐)</label>
228
228
  <input type="file" id="dir" webkitdirectory directory multiple>
229
229
  <button id="dbtn" type="submit">开始体检 →</button>
230
+ <div id="status" class="status"></div>
230
231
  <p class="hint">📂 直接选你的项目文件夹,无需敲路径。文件仅发送到<b>本机的本地服务</b>处理,<b>不经过任何外部服务器、不出本机</b>。</p>
231
232
  </form>
232
233
  <div class="or">— 或 —</div>` : '';
@@ -243,17 +244,20 @@ function formPage(local) {
243
244
  ${local ? UPLOAD_SCRIPT : ''}`);
244
245
  }
245
246
  // 客户端:读取所选文件夹 → 过滤(跳过 node_modules 等、仅文本/配置、限大小) → POST 到本机服务
247
+ // 注意:过滤后缀须与服务端 SCAN_EXT 对齐(含 .md),否则 markdown 项目会被全滤光显得"扫不了"。
246
248
  const UPLOAD_SCRIPT = `<script>
247
249
  (function(){
248
250
  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;
251
+ 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
252
  var ENV=/(^|\\/)\\.env(\\.|$)/; var DEP=/^(package\\.json|requirements\\.txt|pyproject\\.toml|go\\.mod)$/;
251
253
  var form=document.getElementById('dirform'); if(!form) return;
254
+ var statusEl=document.getElementById('status');
255
+ function s(m){ if(statusEl){statusEl.textContent=m;statusEl.style.display='block';} }
252
256
  form.addEventListener('submit', async function(e){
253
257
  e.preventDefault();
254
258
  var inp=document.getElementById('dir'), btn=document.getElementById('dbtn');
255
- if(!inp.files||!inp.files.length){alert('请先选择项目文件夹');return;}
256
- btn.disabled=true; btn.textContent='读取中…';
259
+ if(!inp.files||!inp.files.length){ s('请先点上方按钮选择项目文件夹'); return; }
260
+ btn.disabled=true;
257
261
  var picked=[], total=0, root='';
258
262
  for(var i=0;i<inp.files.length;i++){
259
263
  var f=inp.files[i], rel=f.webkitRelativePath||f.name; if(!root)root=rel.split('/')[0];
@@ -264,13 +268,17 @@ const UPLOAD_SCRIPT = `<script>
264
268
  if(picked.length>=3000||total>8388608) break;
265
269
  total+=f.size; picked.push(f);
266
270
  }
267
- if(!picked.length){alert('未找到可扫描的源码/配置文件');btn.disabled=false;btn.textContent='开始体检 →';return;}
268
- btn.textContent='扫描中… ('+picked.length+' 个文件)';
271
+ if(!picked.length){ s('未找到可扫描的源码/配置文件(已自动跳过 node_modules、图片、超大文件)。请选含代码或配置的目录。'); btn.disabled=false; return; }
272
+ s('读取 '+picked.length+' 个文件…');
269
273
  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(_){} }
274
+ s('扫描中…('+out.length+' 个文件,请稍候)');
270
275
  try{
271
276
  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='开始体检 →'; }
277
+ if(!resp.ok){ s('扫描失败:HTTP '+resp.status+'。请重试,或改用命令行 npx shellward scan。'); btn.disabled=false; return; }
278
+ var html=await resp.text();
279
+ // 用 Blob URL 跳转展示报告(比 document.write 可靠)
280
+ window.location.href=URL.createObjectURL(new Blob([html],{type:'text/html'}));
281
+ }catch(err){ s('扫描失败:'+(err&&err.message||err)+'。请重试。'); btn.disabled=false; }
274
282
  });
275
283
  })();
276
284
  </script>`;
@@ -302,6 +310,8 @@ button{background:#cb0000;color:#fff;border:0;border-radius:10px;padding:14px;fo
302
310
  font-weight:700;cursor:pointer;margin-top:4px}button:hover{background:#a80000}
303
311
  button:disabled{background:#94a3b8;cursor:default}
304
312
  form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6px 0 14px}
313
+ .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
314
+ color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
305
315
  .foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
306
316
  .back{font-weight:600}
307
317
  </style></head><body>${body}</body></html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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",
@@ -65,6 +65,7 @@
65
65
  "test:sdk": "npx tsx test-sdk.ts",
66
66
  "test:mcp": "npx tsx test-mcp.ts",
67
67
  "bench": "npx tsx bench/run.ts",
68
+ "bench:scan": "npx tsx bench/scan-bench.ts",
68
69
  "prepublishOnly": "npm run build"
69
70
  },
70
71
  "openclaw": {
@@ -212,6 +212,7 @@ function formPage(local: boolean): string {
212
212
  <label>① 选择本地项目文件夹(推荐)</label>
213
213
  <input type="file" id="dir" webkitdirectory directory multiple>
214
214
  <button id="dbtn" type="submit">开始体检 →</button>
215
+ <div id="status" class="status"></div>
215
216
  <p class="hint">📂 直接选你的项目文件夹,无需敲路径。文件仅发送到<b>本机的本地服务</b>处理,<b>不经过任何外部服务器、不出本机</b>。</p>
216
217
  </form>
217
218
  <div class="or">— 或 —</div>` : ''
@@ -230,17 +231,20 @@ function formPage(local: boolean): string {
230
231
  }
231
232
 
232
233
  // 客户端:读取所选文件夹 → 过滤(跳过 node_modules 等、仅文本/配置、限大小) → POST 到本机服务
234
+ // 注意:过滤后缀须与服务端 SCAN_EXT 对齐(含 .md),否则 markdown 项目会被全滤光显得"扫不了"。
233
235
  const UPLOAD_SCRIPT = `<script>
234
236
  (function(){
235
237
  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;
238
+ 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
239
  var ENV=/(^|\\/)\\.env(\\.|$)/; var DEP=/^(package\\.json|requirements\\.txt|pyproject\\.toml|go\\.mod)$/;
238
240
  var form=document.getElementById('dirform'); if(!form) return;
241
+ var statusEl=document.getElementById('status');
242
+ function s(m){ if(statusEl){statusEl.textContent=m;statusEl.style.display='block';} }
239
243
  form.addEventListener('submit', async function(e){
240
244
  e.preventDefault();
241
245
  var inp=document.getElementById('dir'), btn=document.getElementById('dbtn');
242
- if(!inp.files||!inp.files.length){alert('请先选择项目文件夹');return;}
243
- btn.disabled=true; btn.textContent='读取中…';
246
+ if(!inp.files||!inp.files.length){ s('请先点上方按钮选择项目文件夹'); return; }
247
+ btn.disabled=true;
244
248
  var picked=[], total=0, root='';
245
249
  for(var i=0;i<inp.files.length;i++){
246
250
  var f=inp.files[i], rel=f.webkitRelativePath||f.name; if(!root)root=rel.split('/')[0];
@@ -251,13 +255,17 @@ const UPLOAD_SCRIPT = `<script>
251
255
  if(picked.length>=3000||total>8388608) break;
252
256
  total+=f.size; picked.push(f);
253
257
  }
254
- if(!picked.length){alert('未找到可扫描的源码/配置文件');btn.disabled=false;btn.textContent='开始体检 →';return;}
255
- btn.textContent='扫描中… ('+picked.length+' 个文件)';
258
+ if(!picked.length){ s('未找到可扫描的源码/配置文件(已自动跳过 node_modules、图片、超大文件)。请选含代码或配置的目录。'); btn.disabled=false; return; }
259
+ s('读取 '+picked.length+' 个文件…');
256
260
  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(_){} }
261
+ s('扫描中…('+out.length+' 个文件,请稍候)');
257
262
  try{
258
263
  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='开始体检 →'; }
264
+ if(!resp.ok){ s('扫描失败:HTTP '+resp.status+'。请重试,或改用命令行 npx shellward scan。'); btn.disabled=false; return; }
265
+ var html=await resp.text();
266
+ // 用 Blob URL 跳转展示报告(比 document.write 可靠)
267
+ window.location.href=URL.createObjectURL(new Blob([html],{type:'text/html'}));
268
+ }catch(err){ s('扫描失败:'+(err&&err.message||err)+'。请重试。'); btn.disabled=false; }
261
269
  });
262
270
  })();
263
271
  </script>`
@@ -291,6 +299,8 @@ button{background:#cb0000;color:#fff;border:0;border-radius:10px;padding:14px;fo
291
299
  font-weight:700;cursor:pointer;margin-top:4px}button:hover{background:#a80000}
292
300
  button:disabled{background:#94a3b8;cursor:default}
293
301
  form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6px 0 14px}
302
+ .status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
303
+ color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
294
304
  .foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
295
305
  .back{font-weight:600}
296
306
  </style></head><body>${body}</body></html>`