shellward 0.7.6 → 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 +1 -1
- package/dist/compliance/html-report.js +5 -1
- package/dist/compliance/project-scan.js +0 -0
- package/dist/web/scan-server.js +70 -12
- package/package.json +1 -1
- package/src/compliance/html-report.ts +5 -1
- package/src/compliance/project-scan.ts +0 -0
- package/src/web/scan-server.ts +65 -12
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/shellward)
|
|
10
10
|
[](./LICENSE)
|
|
11
|
-
[](#performance)
|
|
12
12
|
[](#performance)
|
|
13
13
|
|
|
14
14
|
**🌐 官网: https://jnmetacode.github.io/shellward/**
|
|
@@ -284,8 +284,12 @@ section,.reg{padding:0 36px}
|
|
|
284
284
|
.tbl .muted{color:var(--muted)}
|
|
285
285
|
.tbl .faint{color:var(--faint);font-size:13px}
|
|
286
286
|
.loc code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;
|
|
287
|
-
background:#f1f5f9;color:#0f172a;padding:2px 7px;border-radius:5px;
|
|
287
|
+
background:#f1f5f9;color:#0f172a;padding:2px 7px;border-radius:5px;
|
|
288
|
+
white-space:normal;word-break:break-all;overflow-wrap:anywhere}
|
|
288
289
|
.alts th:first-child,.alts td:first-child{width:120px}
|
|
290
|
+
/* 发现表三列布局:位置≤40% 可换行、说明占主、严重度窄列不挤 */
|
|
291
|
+
table.tbl td.loc{width:34%;max-width:300px}
|
|
292
|
+
table.tbl td.right{width:64px}
|
|
289
293
|
|
|
290
294
|
/* severity 标签 */
|
|
291
295
|
.sev{display:inline-block;font-size:11.5px;font-weight:700;padding:2px 9px;border-radius:999px}
|
|
Binary file
|
package/dist/web/scan-server.js
CHANGED
|
@@ -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
|
-
<
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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 ?
|
|
313
|
+
${local ? BROWSE_SCRIPT : ''}`);
|
|
282
314
|
}
|
|
283
|
-
//
|
|
284
|
-
|
|
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.
|
|
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": [
|
|
@@ -321,8 +321,12 @@ section,.reg{padding:0 36px}
|
|
|
321
321
|
.tbl .muted{color:var(--muted)}
|
|
322
322
|
.tbl .faint{color:var(--faint);font-size:13px}
|
|
323
323
|
.loc code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;
|
|
324
|
-
background:#f1f5f9;color:#0f172a;padding:2px 7px;border-radius:5px;
|
|
324
|
+
background:#f1f5f9;color:#0f172a;padding:2px 7px;border-radius:5px;
|
|
325
|
+
white-space:normal;word-break:break-all;overflow-wrap:anywhere}
|
|
325
326
|
.alts th:first-child,.alts td:first-child{width:120px}
|
|
327
|
+
/* 发现表三列布局:位置≤40% 可换行、说明占主、严重度窄列不挤 */
|
|
328
|
+
table.tbl td.loc{width:34%;max-width:300px}
|
|
329
|
+
table.tbl td.right{width:64px}
|
|
326
330
|
|
|
327
331
|
/* severity 标签 */
|
|
328
332
|
.sev{display:inline-block;font-size:11.5px;font-weight:700;padding:2px 9px;border-radius:999px}
|
|
Binary file
|
package/src/web/scan-server.ts
CHANGED
|
@@ -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
|
-
<
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 ?
|
|
291
|
+
${local ? BROWSE_SCRIPT : ''}`)
|
|
266
292
|
}
|
|
267
293
|
|
|
268
|
-
//
|
|
269
|
-
|
|
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>`
|