Skip to content

Commit c1f4db4

Browse files
committed
fix(scan): 诚实评分 + 扫 .md + URL 健壮性 (v0.7.2)
- 静态扫描不再报"优秀/A",以项目实测风险为主指标,明确标注未验证项/非完整合规结论 - 纳入 .md/.mdx/.ipynb 扫描(此前跳过markdown,prompt项目没真扫正文);superpowers-zh 74→150文件 - markdown 文档只检境外端点、跳过密钥/PII 模式,避免README示例误报 - web URL: 克隆超时30→60s、并发2→4、大仓库友好提示引导本地客户端、表单防重复点击 - 用户反馈"上传就出结果太假"后修正; test 85→94, 全套282全绿
1 parent 481e402 commit c1f4db4

9 files changed

Lines changed: 103 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ 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.2] - 2026-06-20
9+
10+
### Changed — 诚实度与真实覆盖(用户反馈"上传就出结果太假"后修正)
11+
- **静态扫描不再报"优秀/A"式合规结论**:以「项目实测风险」为主指标(未发现/发现 N 项),得分降为"可观测项"次要参考,并明确标注「X 项合规控制项未验证、非完整合规结论」(终端 + HTML 一致)
12+
- **扫描 `.md`/`.mdx`/`.ipynb`**:此前完全跳过 markdown,导致 prompt/skill 类项目"没真扫到正文"。现纳入扫描(superpowers-zh 扫描文件数 74→150)
13+
- **Markdown 文档只检测境外端点、跳过密钥/PII 模式**:避免把 README「检测示例」里的演示密钥/SSN 误报为真实风险(ShellWard 自扫从误报回到 0)
14+
- **Web URL 扫描健壮性**:克隆超时 30s→60s、并发 2→4、大仓库/超时给友好提示(引导用本地客户端选文件夹)、URL 表单加"扫描中"状态防重复点击导致 503
15+
- `test-compliance.ts` 扩至 94 项;全套 **282 测试**全绿
16+
817
## [0.7.1] - 2026-06-20
918

1019
### Changed — 本地客户端 UX:选文件夹上传(不再手敲路径)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
[![npm](https://img.shields.io/npm/v/shellward?color=cb0000&label=npm)](https://www.npmjs.com/package/shellward)
1010
[![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
11-
[![tests](https://img.shields.io/badge/tests-273%20passing-brightgreen)](#performance)
11+
[![tests](https://img.shields.io/badge/tests-282%20passing-brightgreen)](#performance)
1212
[![deps](https://img.shields.io/badge/dependencies-0-brightgreen)](#performance)
1313

1414
**🌐 官网: https://jnmetacode.github.io/shellward/**

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.1",
3+
"version": "0.7.2",
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/audit.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export interface ComplianceReport {
5757
generatedAt: string
5858
/** 项目实测风险造成的扣分(仅项目体检路径);0 表示纯控制项评分 */
5959
projectPenalty?: number
60+
/** 是否为静态扫描(未部署运行时):此时多数控制项不可验证,得分不代表完整合规 */
61+
staticScan?: boolean
62+
/** 项目扫描的文件总数(静态扫描路径) */
63+
filesScanned?: number
6064
}
6165

6266
/** 层能力映射:控制项 id → 必须启用的层(全部启用才 pass,部分启用 warn,全关 fail) */
@@ -191,6 +195,8 @@ export function runProjectComplianceAudit(config: ShellWardConfig, root: string)
191195
report.grade = gradeOf(report.score)
192196
report.projectPenalty = penalty
193197
}
198+
report.staticScan = true
199+
report.filesScanned = scan.filesScanned
194200

195201
return { report, scan }
196202
}

src/compliance/html-report.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,25 +60,37 @@ export function renderHtmlReport(
6060
const S: string[] = []
6161

6262
// ===== 评分 Hero =====
63+
// 诚实原则:静态扫描下多数控制项不可验证,不展示"优秀/A"式合规结论,
64+
// 改以「风险发现数」为主指标,得分仅作"可观测项"的次要参考。
65+
const findingsN = scan.findings.length
66+
const gradeLabel = report.staticScan ? t('可观测项', 'observable') : t(g.zh, g.en)
67+
const verdict = report.staticScan
68+
? (findingsN === 0
69+
? { txt: t('未发现可观测风险', 'No observable risks'), c: '#16a34a', ic: '🟢' }
70+
: { txt: t(`发现 ${findingsN} 项风险`, `${findingsN} risk(s) found`), c: '#dc2626', ic: '🔴' })
71+
: { txt: t(g.zh, g.en), c: g.color, ic: '' }
72+
6373
S.push(`
6474
<section class="hero">
6575
<div class="gauge" style="--p:${report.score};--c:${g.color}">
6676
<div class="gauge-in">
6777
<div class="gscore">${report.score}<small>/100</small></div>
68-
<div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${t(g.zh, g.en)}</div>
78+
<div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${esc(gradeLabel)}</div>
6979
</div>
7080
</div>
7181
<div class="hero-side">
82+
<div class="verdict" style="--vc:${verdict.c}">${verdict.ic} ${esc(verdict.txt)}</div>
7283
<div class="stat-row">
7384
${stat('pass', '🟢', t('合规', 'Pass'), report.passed)}
7485
${stat('warn', '🟡', t('部分', 'Partial'), report.warned)}
7586
${stat('fail', '🔴', t('不合规', 'Fail'), report.failed)}
76-
${stat('manual', '⚪', t('待确认', 'Review'), report.manual)}
87+
${stat('manual', '⚪', t('待核验', 'Review'), report.manual)}
7788
</div>
7889
${report.projectPenalty ? `<div class="penalty">⚠ ${t('含项目实测风险扣分', 'Includes project-scan penalty')} <b>−${report.projectPenalty}</b></div>` : ''}
79-
<p class="hero-note">${t(
80-
'得分基于本次可静态观测的项目风险。⚪ 待确认项需把 ShellWard 部署为运行时防护或人工核验。',
81-
'Score reflects statically-observable project risk. ⚪ items need runtime deployment or manual review.')}</p>
90+
<p class="hero-note">${report.staticScan
91+
? t(`⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? scan.filesScanned} 个文件,仅评估可观测风险。<b>${report.manual} 项合规控制项未验证</b>(需部署 ShellWard 运行时或人工核验)——本报告不构成完整合规结论,得分仅供参考。`,
92+
`⚠ Static scan: checked ${report.filesScanned ?? scan.filesScanned} files for observable risk only. <b>${report.manual} controls unverified</b> — not a complete compliance verdict.`)
93+
: t('得分基于已部署运行时的合规评估。', 'Score based on deployed-runtime assessment.')}</p>
8294
</div>
8395
</section>`)
8496

@@ -263,6 +275,7 @@ section,.reg{padding:0 36px}
263275
.gscore small{font-size:15px;font-weight:500;color:var(--faint)}
264276
.ggrade{font-size:14px;font-weight:700;margin-top:6px}
265277
.hero-side{flex:1;min-width:0}
278+
.verdict{font-size:17px;font-weight:800;color:var(--vc);margin:0 0 12px}
266279
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
267280
.stat{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;text-align:center}
268281
.stat .sn{font-size:22px;font-weight:800;line-height:1}

src/compliance/project-scan.ts

407 Bytes
Binary file not shown.

src/compliance/report.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,38 @@ export function renderComplianceReport(report: ComplianceReport, locale: 'zh' |
4141
const bar = scoreBar(report.score)
4242
L.push(zh ? '## 总评' : '## Overall')
4343
L.push('')
44-
L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`)
45-
L.push('')
46-
L.push('```')
47-
L.push(`${bar} ${report.score}/100 [${report.grade}]`)
48-
L.push('```')
44+
// 诚实原则:静态扫描下不报"优秀/A"式合规结论,以风险发现数为主指标
45+
if (report.staticScan) {
46+
const fn = report.failed + (report.projectPenalty ? 1 : 0)
47+
L.push(zh
48+
? `**项目实测风险: ${report.failed === 0 && !report.projectPenalty ? '🟢 未发现可观测风险' : '🔴 发现风险,详见下方'}**`
49+
: `**Observable risk: ${report.failed === 0 && !report.projectPenalty ? '🟢 none found' : '🔴 see below'}**`)
50+
L.push('')
51+
L.push(`${bar} ${report.score}/100 ${zh ? '(可观测合规项,仅供参考)' : '(observable controls only)'}`)
52+
void fn
53+
} else {
54+
L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`)
55+
L.push('')
56+
L.push('```')
57+
L.push(`${bar} ${report.score}/100 [${report.grade}]`)
58+
L.push('```')
59+
}
4960
L.push('')
5061
L.push(zh
51-
? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待确认 ${report.manual} (共 ${report.total} 项)`
52-
: `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Manual ${report.manual} (${report.total} controls)`)
62+
? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待核验 ${report.manual} (共 ${report.total} 项)`
63+
: `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Review ${report.manual} (${report.total} controls)`)
5364
if (report.projectPenalty && report.projectPenalty > 0) {
5465
L.push('')
5566
L.push(zh
5667
? `> 含项目实测风险扣分 **-${report.projectPenalty}**(见下方「项目实测风险」)`
5768
: `> Includes **-${report.projectPenalty}** from project scan findings (see Project Scan Findings)`)
5869
}
70+
if (report.staticScan) {
71+
L.push('')
72+
L.push(zh
73+
? `> ⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? '?'} 个文件,仅评估可观测风险;**${report.manual} 项合规控制项未验证**(需部署 ShellWard 运行时或人工核验)。本报告不构成完整合规结论。`
74+
: `> ⚠ Static scan: ${report.filesScanned ?? '?'} files checked; **${report.manual} controls unverified**. Not a complete compliance verdict.`)
75+
}
5976
L.push('')
6077

6178
// ===== 优先整改(fail 项,按严重度) =====

src/web/scan-server.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import { renderHtmlReport } from '../compliance/html-report.js'
2323
import { DEFAULT_CONFIG, resolveLocale } from '../types.js'
2424

2525
const REPO_RE = /^https:\/\/(github\.com|gitlab\.com|gitee\.com|bitbucket\.org)\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?\/?$/
26-
const CLONE_TIMEOUT_MS = 30_000
27-
const MAX_CONCURRENT = 2
26+
const CLONE_TIMEOUT_MS = 60_000
27+
const MAX_CONCURRENT = 4
2828

2929
export interface WebServerOptions {
3030
port: number
@@ -105,7 +105,10 @@ async function handleRepo(res: any, repo: string, locale: 'zh' | 'en', inc: () =
105105
const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
106106
send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }))
107107
} catch (e: any) {
108-
send(res, 502, 'text/html', errorPage('克隆/扫描失败:' + esc(e?.message || String(e)) + '。请确认是可公开访问的仓库。'))
108+
const msg = esc(e?.message || String(e))
109+
send(res, 502, 'text/html', errorPage(
110+
`克隆/扫描失败:${msg}。<br><br>可能原因:仓库过大(克隆超时 60s)、私有仓库、或地址有误。<br>` +
111+
`<b>大仓库 / 私有代码请用本地客户端</b>(选文件夹、不上传):<code>npx shellward web --local</code>,或命令行 <code>npx shellward scan</code>。`))
109112
} finally {
110113
dec()
111114
try { rmSync(dir, { recursive: true, force: true }) } catch { /* ignore */ }
@@ -197,11 +200,11 @@ function send(res: any, code: number, type: string, body: string) {
197200

198201
function formPage(local: boolean): string {
199202
const urlForm = `
200-
<form action="/scan" method="get">
203+
<form action="/scan" method="get" onsubmit="var b=this.querySelector('button');b.disabled=true;b.textContent='扫描中…(大仓库需 10–60 秒,请勿重复点击)';">
201204
<label>${local ? '② ' : ''}公开仓库地址</label>
202205
<input name="repo" placeholder="https://github.com/owner/repo"${local ? '' : ' autofocus'}>
203206
<button type="submit">${local ? '体检该仓库 →' : '开始体检 →'}</button>
204-
<p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。${local ? '' : '<b>私有/敏感代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
207+
<p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时改用上方「选择文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
205208
</form>`
206209

207210
const uploadForm = local ? `

test-compliance.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,42 @@ console.log('\n--- web 扫描器 URL 校验 ---')
358358
test('命令注入字符 → 拒绝', !ok('https://github.com/a/b;rm -rf'))
359359
}
360360

361+
// === 13. Markdown 文档扫描策略 + 诚实静态评分 ===
362+
console.log('\n--- Markdown 策略 + 诚实评分 ---')
363+
{
364+
const dir = mkdtempSync(join(tmpdir(), 'sw-md-'))
365+
try {
366+
// README 文档里的示例密钥/PII 不应误报;但 .md 里真实境外端点应报
367+
writeFileSync(join(dir, 'README.md'),
368+
'示例: password: MyP@ssw0rd123 手机 13912345678 SSN 123-45-6789\n调用 https://api.openai.com/v1\n')
369+
// 真实代码文件里的密钥仍要报
370+
writeFileSync(join(dir, 'app.ts'), 'const k="sk-RZ9mKp2QwLs7Yv3Nd8Tb1Hc4Xj6Pq"\n')
371+
const scan = scanProject(dir)
372+
const mdSecret = scan.findings.some(f => f.file === 'README.md' && (f.kind === 'secret' || f.kind === 'pii'))
373+
test('Markdown 文档示例密钥/PII 不误报', !mdSecret)
374+
test('Markdown 里真实境外端点仍检出', scan.findings.some(f => f.file === 'README.md' && f.kind === 'overseas'))
375+
test('代码文件密钥仍检出', scan.findings.some(f => f.file === 'app.ts' && f.kind === 'secret'))
376+
test('被扫描文件数包含 .md', scan.filesScanned >= 2)
377+
378+
// 诚实静态评分:不报"优秀",标记 staticScan
379+
const { report } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
380+
test('体检标记 staticScan', report.staticScan === true)
381+
test('体检记录 filesScanned', (report.filesScanned ?? 0) >= 2)
382+
const html = renderHtmlReport(report, scan, 'zh', { root: dir })
383+
test('HTML 不含"优秀"误导词', !html.includes('优秀'))
384+
test('HTML 含静态扫描诚实声明', html.includes('本次为静态扫描') && html.includes('未验证'))
385+
} finally { rmSync(dir, { recursive: true, force: true }) }
386+
387+
// 干净项目静态扫描:HTML 应显示"未发现可观测风险"而非"优秀"
388+
const clean = mkdtempSync(join(tmpdir(), 'sw-cl2-'))
389+
try {
390+
writeFileSync(join(clean, 'index.js'), 'console.log("hi")\n')
391+
const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, clean)
392+
const html = renderHtmlReport(report, scan, 'zh', { root: clean })
393+
test('干净静态扫描 HTML 显示"未发现可观测风险"', html.includes('未发现可观测风险'))
394+
} finally { rmSync(clean, { recursive: true, force: true }) }
395+
}
396+
361397
// === 总结 ===
362398
console.log(`\n========== 结果: ${passed} 通过, ${failed} 失败 ==========\n`)
363399
if (failed > 0) process.exit(1)

0 commit comments

Comments
 (0)