shellward 0.7.7 → 0.7.9
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/audit.js +3 -2
- package/dist/compliance/html-report.js +7 -1
- package/dist/compliance/report.js +6 -0
- package/dist/web/scan-server.js +70 -12
- package/package.json +1 -1
- package/src/compliance/audit.ts +3 -4
- package/src/compliance/html-report.ts +9 -1
- package/src/compliance/report.ts +6 -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/**
|
package/dist/compliance/audit.js
CHANGED
|
@@ -141,9 +141,10 @@ function computeProjectPenalty(scan) {
|
|
|
141
141
|
return Math.min(MAX_PROJECT_PENALTY, p);
|
|
142
142
|
}
|
|
143
143
|
function checkControl(c, config, env, deployed) {
|
|
144
|
-
// 静态扫描(未部署运行时)下,能力层/审计日志类控制项无法验证 —— 标为顾问态,绝不虚报"已合规"
|
|
144
|
+
// 静态扫描(未部署运行时)下,能力层/审计日志类控制项无法验证 —— 标为顾问态,绝不虚报"已合规"。
|
|
145
|
+
// 「为何待核验」统一在报告区块开头说一次;这里每行只留"该做什么",避免 12 行重复同一句。
|
|
145
146
|
if (!deployed && (c.method === 'capability' || c.method === 'config' || c.method === 'audit')) {
|
|
146
|
-
return mk(c, 'manual',
|
|
147
|
+
return mk(c, 'manual', c.remediation_zh, c.remediation_en);
|
|
147
148
|
}
|
|
148
149
|
switch (c.method) {
|
|
149
150
|
case 'capability': return checkCapability(c, config);
|
|
@@ -137,7 +137,10 @@ export function renderHtmlReport(report, scan, locale, meta) {
|
|
|
137
137
|
S.push(`<p class="note">💡 ${t('对使用 openai SDK 的项目:通常仅需把 base_url 与 api_key 换成上表任一境内模型即可,业务代码无需改动。', 'For openai-SDK projects: usually just swap base_url + api_key — no code change.')}</p>`);
|
|
138
138
|
}
|
|
139
139
|
// ===== 控制项明细 =====
|
|
140
|
-
S.push(sectionHead('📋', t('合规控制项明细', 'Compliance Controls'), t('
|
|
140
|
+
S.push(sectionHead('📋', t('合规控制项明细', 'Compliance Controls'), t('按法规分组', 'By regulation')));
|
|
141
|
+
if (report.staticScan && report.manual > 0) {
|
|
142
|
+
S.push(`<div class="note manual-note">${t(`<b>⚪ 待核验 ≠ 不合规。</b> 下方 ${report.manual} 项是<b>运行时合规控制</b>(审计留存、内容过滤、注入拦截、数据外发管控等)——靠"看代码"的静态扫描判断不了,需把 ShellWard 接入你的 AI 应用(<code>npx shellward mcp</code> 或插件)作为运行时防护后才能验证,或人工核验。每项后面是"该做什么"。`, `<b>⚪ Review ≠ non-compliant.</b> The ${report.manual} items below are <b>runtime controls</b> a static scan cannot verify — deploy ShellWard as a runtime guard (<code>npx shellward mcp</code> / plugin) to validate them. Each row shows the remediation.`)}</div>`);
|
|
143
|
+
}
|
|
141
144
|
const grouped = groupBy(report.results);
|
|
142
145
|
for (const reg of REG_ORDER) {
|
|
143
146
|
const items = grouped[reg];
|
|
@@ -312,6 +315,9 @@ table.tbl td.right{width:64px}
|
|
|
312
315
|
.mtag.mid{background:var(--warn-bg);color:var(--warn)}
|
|
313
316
|
.note{margin:8px 36px 4px;font-size:12.5px;color:var(--muted);background:#f8fafc;
|
|
314
317
|
border-left:3px solid var(--brand);padding:10px 14px;border-radius:0 8px 8px 0}
|
|
318
|
+
.manual-note{margin:8px 36px 12px;font-size:13px;color:#475569;background:#eff6ff;
|
|
319
|
+
border-left:3px solid #3b82f6;line-height:1.6}
|
|
320
|
+
.manual-note code{background:#dbeafe;padding:1px 6px;border-radius:5px}
|
|
315
321
|
|
|
316
322
|
/* 法规分组 */
|
|
317
323
|
.reg{margin:14px 36px;padding:0;border:1px solid var(--line);border-radius:12px;overflow:hidden}
|
|
@@ -82,6 +82,12 @@ export function renderComplianceReport(report, locale) {
|
|
|
82
82
|
// ===== 按法规分组明细 =====
|
|
83
83
|
L.push(zh ? '## 分项明细' : '## Detailed Results');
|
|
84
84
|
L.push('');
|
|
85
|
+
if (report.staticScan && report.manual > 0) {
|
|
86
|
+
L.push(zh
|
|
87
|
+
? `> **⚪ 待核验 ≠ 不合规。** 下方 ${report.manual} 项是**运行时合规控制**(审计留存、内容过滤、注入拦截、数据外发管控等),静态扫描(看代码)判断不了,需把 ShellWard 接入你的 AI 应用(\`npx shellward mcp\` 或插件)作为运行时防护后验证,或人工核验。`
|
|
88
|
+
: `> **⚪ Review ≠ non-compliant.** The ${report.manual} items below are runtime controls a static scan cannot verify — deploy ShellWard as a runtime guard to validate.`);
|
|
89
|
+
L.push('');
|
|
90
|
+
}
|
|
85
91
|
const grouped = groupByRegulation(report.results);
|
|
86
92
|
for (const reg of REGULATION_ORDER) {
|
|
87
93
|
const items = grouped[reg];
|
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.9",
|
|
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": [
|
package/src/compliance/audit.ts
CHANGED
|
@@ -212,11 +212,10 @@ function computeProjectPenalty(scan: ProjectScanResult): number {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
function checkControl(c: ComplianceControl, config: ShellWardConfig, env: EnvFacts, deployed: boolean): ControlResult {
|
|
215
|
-
// 静态扫描(未部署运行时)下,能力层/审计日志类控制项无法验证 —— 标为顾问态,绝不虚报"已合规"
|
|
215
|
+
// 静态扫描(未部署运行时)下,能力层/审计日志类控制项无法验证 —— 标为顾问态,绝不虚报"已合规"。
|
|
216
|
+
// 「为何待核验」统一在报告区块开头说一次;这里每行只留"该做什么",避免 12 行重复同一句。
|
|
216
217
|
if (!deployed && (c.method === 'capability' || c.method === 'config' || c.method === 'audit')) {
|
|
217
|
-
return mk(c, 'manual',
|
|
218
|
-
`ShellWard 运行时可提供此防护;当前为静态扫描、未部署,无法验证。整改:${c.remediation_zh}`,
|
|
219
|
-
`Provided by ShellWard runtime; not verifiable in a static scan. ${c.remediation_en}`)
|
|
218
|
+
return mk(c, 'manual', c.remediation_zh, c.remediation_en)
|
|
220
219
|
}
|
|
221
220
|
switch (c.method) {
|
|
222
221
|
case 'capability': return checkCapability(c, config)
|
|
@@ -164,7 +164,12 @@ export function renderHtmlReport(
|
|
|
164
164
|
|
|
165
165
|
// ===== 控制项明细 =====
|
|
166
166
|
S.push(sectionHead('📋', t('合规控制项明细', 'Compliance Controls'),
|
|
167
|
-
t('
|
|
167
|
+
t('按法规分组', 'By regulation')))
|
|
168
|
+
if (report.staticScan && report.manual > 0) {
|
|
169
|
+
S.push(`<div class="note manual-note">${t(
|
|
170
|
+
`<b>⚪ 待核验 ≠ 不合规。</b> 下方 ${report.manual} 项是<b>运行时合规控制</b>(审计留存、内容过滤、注入拦截、数据外发管控等)——靠"看代码"的静态扫描判断不了,需把 ShellWard 接入你的 AI 应用(<code>npx shellward mcp</code> 或插件)作为运行时防护后才能验证,或人工核验。每项后面是"该做什么"。`,
|
|
171
|
+
`<b>⚪ Review ≠ non-compliant.</b> The ${report.manual} items below are <b>runtime controls</b> a static scan cannot verify — deploy ShellWard as a runtime guard (<code>npx shellward mcp</code> / plugin) to validate them. Each row shows the remediation.`)}</div>`)
|
|
172
|
+
}
|
|
168
173
|
const grouped = groupBy(report.results)
|
|
169
174
|
for (const reg of REG_ORDER) {
|
|
170
175
|
const items = grouped[reg]
|
|
@@ -349,6 +354,9 @@ table.tbl td.right{width:64px}
|
|
|
349
354
|
.mtag.mid{background:var(--warn-bg);color:var(--warn)}
|
|
350
355
|
.note{margin:8px 36px 4px;font-size:12.5px;color:var(--muted);background:#f8fafc;
|
|
351
356
|
border-left:3px solid var(--brand);padding:10px 14px;border-radius:0 8px 8px 0}
|
|
357
|
+
.manual-note{margin:8px 36px 12px;font-size:13px;color:#475569;background:#eff6ff;
|
|
358
|
+
border-left:3px solid #3b82f6;line-height:1.6}
|
|
359
|
+
.manual-note code{background:#dbeafe;padding:1px 6px;border-radius:5px}
|
|
352
360
|
|
|
353
361
|
/* 法规分组 */
|
|
354
362
|
.reg{margin:14px 36px;padding:0;border:1px solid var(--line);border-radius:12px;overflow:hidden}
|
package/src/compliance/report.ts
CHANGED
|
@@ -93,6 +93,12 @@ export function renderComplianceReport(report: ComplianceReport, locale: 'zh' |
|
|
|
93
93
|
// ===== 按法规分组明细 =====
|
|
94
94
|
L.push(zh ? '## 分项明细' : '## Detailed Results')
|
|
95
95
|
L.push('')
|
|
96
|
+
if (report.staticScan && report.manual > 0) {
|
|
97
|
+
L.push(zh
|
|
98
|
+
? `> **⚪ 待核验 ≠ 不合规。** 下方 ${report.manual} 项是**运行时合规控制**(审计留存、内容过滤、注入拦截、数据外发管控等),静态扫描(看代码)判断不了,需把 ShellWard 接入你的 AI 应用(\`npx shellward mcp\` 或插件)作为运行时防护后验证,或人工核验。`
|
|
99
|
+
: `> **⚪ Review ≠ non-compliant.** The ${report.manual} items below are runtime controls a static scan cannot verify — deploy ShellWard as a runtime guard to validate.`)
|
|
100
|
+
L.push('')
|
|
101
|
+
}
|
|
96
102
|
|
|
97
103
|
const grouped = groupByRegulation(report.results)
|
|
98
104
|
for (const reg of REGULATION_ORDER) {
|
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>`
|