Skip to content

Commit 593cdb8

Browse files
committed
feat(report): 检查过程可见 + 含风险演示 (v0.7.6)
- 空结果展示逐项检查清单(查了什么+✓0命中+文件/规则/耗时),回应"秒出=没检查吗" - 内置 /demo 含风险示例一键演示(满屏发现+行号),证明快≠假 - scan-server.ts 加入 .shellwardignore(演示样例含密钥字面量) - 全套300测试全绿
1 parent dedf01b commit 593cdb8

6 files changed

Lines changed: 72 additions & 4 deletions

File tree

.shellwardignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ src/rules/injection-en.ts
1313
src/rules/dangerous-commands.ts
1414
src/rules/protected-paths.ts
1515
src/rules/tool-poisoning.ts
16+
src/web/scan-server.ts
1617

1718
# 测试、基准语料、演示(故意包含攻击样本/示例密钥)
1819
test-*.ts

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/),
66
and this project adheres to [Semantic Versioning](https://semver.org/).
77

8+
## [0.7.6] - 2026-06-20
9+
10+
### Added — 让"检查过程"可见(回应"秒出=没检查吗")
11+
- **空结果也展示检查过程**:未发现风险时不再只说"未发现",而是逐项列出查了什么(境外端点+SDK依赖38特征 / 硬编码密钥 / 中文+国际PII / .env权限)+ ✓0命中 + 文件数/规则数/耗时——证明确实逐项扫了
12+
- **内置"含风险示例"一键演示** (`/demo` + 首页按钮):扫一个故意埋了风险的样例项目,同样秒出但满屏发现(境外5/密钥6/PII4 + 行号),直观证明"快≠假"
13+
- 验证:独立 grep 确认真实干净项目(superpowers-zh)无风险→报0正确;往副本植入密钥→立刻命中 file:line
14+
815
## [0.7.5] - 2026-06-20
916

1017
### Fixed — web 客户端"扫不了"的真实 bug(用户反馈后修)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "shellward",
3-
"version": "0.7.5",
3+
"version": "0.7.6",
44
"mcpName": "io.github.jnMetaCode/shellward",
55
"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.",
66
"keywords": [

src/compliance/html-report.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,14 @@ export function renderHtmlReport(
100100
`Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`)))
101101

102102
if (scan.findings.length === 0) {
103-
S.push(`<div class="empty">🟢 ${t('未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。',
104-
'No hardcoded secrets, PII, or overseas endpoints found in project files.')}</div>`)
103+
// 空结果也要展示"检查过程"——逐项列出查了什么、均未命中,证明确实扫了
104+
S.push(`<div class="empty">🟢 ${t('逐项检查完成,未在可扫描文件中发现风险。', 'All checks passed — no risks found in scannable files.')}</div>`)
105+
S.push(`<table class="tbl checked"><tbody>
106+
<tr><td>🌐 ${t('境外大模型端点 + SDK 依赖', 'Overseas LLM endpoints + SDK deps')}</td><td class="muted">${t('OpenAI / Anthropic / Gemini / Cohere… 共 38 个特征', '38 signatures')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
107+
<tr><td>🔑 ${t('硬编码密钥', 'Hardcoded secrets')}</td><td class="muted">${t('OpenAI/GitHub/AWS key、私钥、JWT、口令、连接串', 'OpenAI/GitHub/AWS/private key/JWT/password/conn-string')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
108+
<tr><td>🪪 ${t('中文 PII + 国际 PII', 'Chinese + intl PII')}</td><td class="muted">${t('身份证(校验位)/手机号/银行卡(Luhn)/SSN/信用卡', 'CN ID(checksum)/mobile/UnionPay(Luhn)/SSN/credit card')}</td><td class="right ok">✓ ${t('0 命中', '0 hits')}</td></tr>
109+
<tr><td>📂 .env ${t('权限', 'permission')}</td><td class="muted">${t('含密钥的 .env 不应组/其他可读', '.env should not be group/other readable')}</td><td class="right ok">✓ ${t('正常', 'OK')}</td></tr>
110+
</tbody></table>`)
105111
} else {
106112
S.push('<div class="chips">')
107113
for (const k of KIND_ORDER) if (scan.counts[k] > 0) {
@@ -296,6 +302,8 @@ section,.reg{padding:0 36px}
296302
padding:0 8px;border-radius:999px;margin-left:4px;font-weight:600}
297303
.empty{margin:8px 36px;padding:16px 18px;background:var(--pass-bg);color:var(--pass);
298304
border-radius:10px;font-weight:600;font-size:14px}
305+
.checked td.ok{color:var(--pass);font-weight:700}
306+
.checked td:first-child{font-weight:600;white-space:nowrap}
299307
300308
/* chips 概览 */
301309
.chips{display:flex;flex-wrap:wrap;gap:8px;margin:6px 36px 4px}

src/compliance/report.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,23 @@ export function renderProjectFindings(scan: ProjectScanResult, locale: 'zh' | 'e
146146
L.push('')
147147

148148
if (scan.findings.length === 0) {
149-
L.push(zh ? '🟢 未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。' : '🟢 No hardcoded secrets, PII exposure, or overseas endpoints found in project files.')
149+
L.push(zh ? '🟢 逐项检查完成,未在可扫描文件中发现风险:' : '🟢 All checks passed — no risks found:')
150+
L.push('')
151+
if (zh) {
152+
L.push('| 检查项 | 覆盖 | 结果 |')
153+
L.push('|---|---|---|')
154+
L.push('| 🌐 境外大模型端点 + SDK 依赖 | OpenAI/Anthropic/Gemini… 38 个特征 | ✓ 0 命中 |')
155+
L.push('| 🔑 硬编码密钥 | OpenAI/GitHub/AWS key、私钥、JWT、口令、连接串 | ✓ 0 命中 |')
156+
L.push('| 🪪 中文+国际 PII | 身份证(校验位)/手机号/银行卡(Luhn)/SSN/信用卡 | ✓ 0 命中 |')
157+
L.push('| 📂 .env 权限 | 含密钥的 .env 不应组/其他可读 | ✓ 正常 |')
158+
} else {
159+
L.push('| Check | Coverage | Result |')
160+
L.push('|---|---|---|')
161+
L.push('| 🌐 Overseas endpoints + SDK deps | 38 signatures | ✓ 0 hits |')
162+
L.push('| 🔑 Hardcoded secrets | OpenAI/GitHub/AWS/private key/JWT/password | ✓ 0 hits |')
163+
L.push('| 🪪 PII (CN + intl) | CN ID/mobile/UnionPay/SSN/credit card | ✓ 0 hits |')
164+
L.push('| 📂 .env permission | group/other-readable check | ✓ OK |')
165+
}
150166
L.push('')
151167
return L.join('\n')
152168
}

src/web/scan-server.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export function startWebServer(opts: WebServerOptions): void {
5858
if (active >= MAX_CONCURRENT) return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试'))
5959
return await handleUpload(req, res, locale, () => { active++ }, () => { active-- })
6060
}
61+
// 演示:扫一个内置的「含风险样例项目」——证明"秒出≠没检查"(满屏发现 + 行号)
62+
if (u.pathname === '/demo') {
63+
if (active >= MAX_CONCURRENT) return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试'))
64+
return handleDemo(res, locale, () => { active++ }, () => { active-- })
65+
}
6166
if (u.pathname === '/scan') {
6267
if (active >= MAX_CONCURRENT) {
6368
return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试(并发上限)'))
@@ -175,6 +180,35 @@ async function handleUpload(req: any, res: any, locale: 'zh' | 'en', inc: () =>
175180
}
176181
}
177182

183+
/** 演示:内置「含风险样例项目」扫描,证明检测真在工作 */
184+
function handleDemo(res: any, locale: 'zh' | 'en', inc: () => void, dec: () => void) {
185+
const dir = mkdtempSync(join(tmpdir(), 'sw-demo-'))
186+
inc()
187+
try {
188+
mkdirSync(join(dir, 'src'), { recursive: true })
189+
mkdirSync(join(dir, 'data'), { recursive: true })
190+
writeFileSync(join(dir, 'package.json'), JSON.stringify({
191+
name: 'demo-ai-app', dependencies: { 'openai': '^4.20.0', '@anthropic-ai/sdk': '^0.20.0', 'express': '^4' },
192+
}, null, 2))
193+
writeFileSync(join(dir, 'src', 'config.ts'),
194+
'export const LLM = "https://api.openai.com/v1"\n'
195+
+ 'const OPENAI_KEY = "sk-Rz9MkP2qWlS7yV3nD8tB1hC4xJ6pQsTuVwYz0"\n'
196+
+ 'const GITHUB_TOKEN = "ghp_Rz9MkP2qWlS7yV3nD8tB1hC4xJ6pQsTuVwYz"\n'
197+
+ 'export const ADMIN_PHONE = "13912345678"\n')
198+
writeFileSync(join(dir, 'data', 'customers.csv'),
199+
'name,id_card,phone,card\n张三,110101199003071233,13800138000,4111111111111111\n')
200+
writeFileSync(join(dir, '.env'),
201+
'AWS_ACCESS_KEY=AKIARZ9MKP2QWLS7YV3N\nDB_PASSWORD=Sup3rS3cretProdPwd2026\n')
202+
const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
203+
send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: '示例项目(含风险)/ demo-ai-app' }))
204+
} catch (e: any) {
205+
send(res, 500, 'text/html', errorPage('演示失败:' + esc(e?.message || String(e))))
206+
} finally {
207+
dec()
208+
try { rmSync(dir, { recursive: true, force: true }) } catch { /* ignore */ }
209+
}
210+
}
211+
178212
/** 浅克隆公开仓库到临时目录(不鉴权、超时、不执行任何仓库代码) */
179213
function cloneRepo(url: string, dir: string): Promise<void> {
180214
return new Promise((res, rej) => {
@@ -224,6 +258,7 @@ function formPage(local: boolean): string {
224258
<p class="sub">${local ? '选项目文件夹或贴公开仓库链接' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
225259
${uploadForm}
226260
${urlForm}
261+
<p class="demo">🤔 觉得"秒出"不真? <a href="/demo">▶ 看一个含风险的示例报告</a>(同样秒出,但满屏发现 + 行号)</p>
227262
<p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
228263
<a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
229264
</div>
@@ -301,6 +336,7 @@ button:disabled{background:#94a3b8;cursor:default}
301336
form{margin:0 0 14px}.or{text-align:center;color:#94a3b8;font-size:13px;margin:6px 0 14px}
302337
.status{display:none;margin:10px 0 0;padding:10px 14px;border-radius:8px;background:#f1f5f9;
303338
color:#334155;font-size:13.5px;border-left:3px solid #cb0000;text-align:left}
339+
.demo{margin:18px 0 0;font-size:13px;color:#475569}.demo a{font-weight:600}
304340
.foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
305341
.back{font-weight:600}
306342
</style></head><body>${body}</body></html>`

0 commit comments

Comments
 (0)