shellward 0.7.8 → 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.
- package/dist/compliance/audit.js +3 -2
- package/dist/compliance/html-report.d.ts +2 -0
- package/dist/compliance/html-report.js +12 -1
- package/dist/compliance/report.js +6 -0
- package/dist/web/scan-server.js +30 -17
- package/package.json +1 -1
- package/src/compliance/audit.ts +3 -4
- package/src/compliance/html-report.ts +16 -1
- package/src/compliance/report.ts +6 -0
- package/src/web/scan-server.ts +30 -17
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);
|
|
@@ -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;
|
|
@@ -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];
|
|
@@ -168,6 +171,7 @@ export function renderHtmlReport(report, scan, locale, meta) {
|
|
|
168
171
|
<style>${CSS}</style>
|
|
169
172
|
</head>
|
|
170
173
|
<body>
|
|
174
|
+
${meta.backLink ? `<div class="backbar"><a href="${esc(meta.backLink)}">← ${t('返回 / 再扫一个', 'Back / scan another')}</a></div>` : ''}
|
|
171
175
|
<main>
|
|
172
176
|
<header>
|
|
173
177
|
<div class="brand">🛡️ Shell<span>Ward</span> <em>${t('合规网关', 'Compliance Gateway')}</em></div>
|
|
@@ -221,6 +225,10 @@ const CSS = `
|
|
|
221
225
|
body{margin:0;background:var(--bg);color:var(--ink);
|
|
222
226
|
font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
|
223
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}}
|
|
224
232
|
main{max-width:880px;margin:28px auto;background:var(--card);border-radius:16px;
|
|
225
233
|
box-shadow:0 1px 3px rgba(15,23,42,.06),0 12px 32px rgba(15,23,42,.07);overflow:hidden}
|
|
226
234
|
header{padding:30px 36px 22px;background:linear-gradient(180deg,#fafbfd,#fff);border-bottom:1px solid var(--line)}
|
|
@@ -312,6 +320,9 @@ table.tbl td.right{width:64px}
|
|
|
312
320
|
.mtag.mid{background:var(--warn-bg);color:var(--warn)}
|
|
313
321
|
.note{margin:8px 36px 4px;font-size:12.5px;color:var(--muted);background:#f8fafc;
|
|
314
322
|
border-left:3px solid var(--brand);padding:10px 14px;border-radius:0 8px 8px 0}
|
|
323
|
+
.manual-note{margin:8px 36px 12px;font-size:13px;color:#475569;background:#eff6ff;
|
|
324
|
+
border-left:3px solid #3b82f6;line-height:1.6}
|
|
325
|
+
.manual-note code{background:#dbeafe;padding:1px 6px;border-radius:5px}
|
|
315
326
|
|
|
316
327
|
/* 法规分组 */
|
|
317
328
|
.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
|
@@ -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 ? '
|
|
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 ? '
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
<
|
|
296
|
-
<
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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 ? '
|
|
307
|
-
${
|
|
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.
|
|
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": [
|
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)
|
|
@@ -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 合规报告 */
|
|
@@ -164,7 +166,12 @@ export function renderHtmlReport(
|
|
|
164
166
|
|
|
165
167
|
// ===== 控制项明细 =====
|
|
166
168
|
S.push(sectionHead('📋', t('合规控制项明细', 'Compliance Controls'),
|
|
167
|
-
t('
|
|
169
|
+
t('按法规分组', 'By regulation')))
|
|
170
|
+
if (report.staticScan && report.manual > 0) {
|
|
171
|
+
S.push(`<div class="note manual-note">${t(
|
|
172
|
+
`<b>⚪ 待核验 ≠ 不合规。</b> 下方 ${report.manual} 项是<b>运行时合规控制</b>(审计留存、内容过滤、注入拦截、数据外发管控等)——靠"看代码"的静态扫描判断不了,需把 ShellWard 接入你的 AI 应用(<code>npx shellward mcp</code> 或插件)作为运行时防护后才能验证,或人工核验。每项后面是"该做什么"。`,
|
|
173
|
+
`<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>`)
|
|
174
|
+
}
|
|
168
175
|
const grouped = groupBy(report.results)
|
|
169
176
|
for (const reg of REG_ORDER) {
|
|
170
177
|
const items = grouped[reg]
|
|
@@ -198,6 +205,7 @@ export function renderHtmlReport(
|
|
|
198
205
|
<style>${CSS}</style>
|
|
199
206
|
</head>
|
|
200
207
|
<body>
|
|
208
|
+
${meta.backLink ? `<div class="backbar"><a href="${esc(meta.backLink)}">← ${t('返回 / 再扫一个', 'Back / scan another')}</a></div>` : ''}
|
|
201
209
|
<main>
|
|
202
210
|
<header>
|
|
203
211
|
<div class="brand">🛡️ Shell<span>Ward</span> <em>${t('合规网关', 'Compliance Gateway')}</em></div>
|
|
@@ -258,6 +266,10 @@ const CSS = `
|
|
|
258
266
|
body{margin:0;background:var(--bg);color:var(--ink);
|
|
259
267
|
font:15px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
|
260
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}}
|
|
261
273
|
main{max-width:880px;margin:28px auto;background:var(--card);border-radius:16px;
|
|
262
274
|
box-shadow:0 1px 3px rgba(15,23,42,.06),0 12px 32px rgba(15,23,42,.07);overflow:hidden}
|
|
263
275
|
header{padding:30px 36px 22px;background:linear-gradient(180deg,#fafbfd,#fff);border-bottom:1px solid var(--line)}
|
|
@@ -349,6 +361,9 @@ table.tbl td.right{width:64px}
|
|
|
349
361
|
.mtag.mid{background:var(--warn-bg);color:var(--warn)}
|
|
350
362
|
.note{margin:8px 36px 4px;font-size:12.5px;color:var(--muted);background:#f8fafc;
|
|
351
363
|
border-left:3px solid var(--brand);padding:10px 14px;border-radius:0 8px 8px 0}
|
|
364
|
+
.manual-note{margin:8px 36px 12px;font-size:13px;color:#475569;background:#eff6ff;
|
|
365
|
+
border-left:3px solid #3b82f6;line-height:1.6}
|
|
366
|
+
.manual-note code{background:#dbeafe;padding:1px 6px;border-radius:5px}
|
|
352
367
|
|
|
353
368
|
/* 法规分组 */
|
|
354
369
|
.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
|
@@ -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 ? '
|
|
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 ? '
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
<
|
|
273
|
-
<
|
|
274
|
-
<
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 ? '
|
|
285
|
-
${
|
|
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}
|