shellward 0.7.1 → 0.7.3

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 CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/shellward?color=cb0000&label=npm)](https://www.npmjs.com/package/shellward)
10
10
  [![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)
12
12
  [![deps](https://img.shields.io/badge/dependencies-0-brightgreen)](#performance)
13
13
 
14
14
  **🌐 官网: https://jnmetacode.github.io/shellward/**
@@ -36,6 +36,10 @@ export interface ComplianceReport {
36
36
  generatedAt: string;
37
37
  /** 项目实测风险造成的扣分(仅项目体检路径);0 表示纯控制项评分 */
38
38
  projectPenalty?: number;
39
+ /** 是否为静态扫描(未部署运行时):此时多数控制项不可验证,得分不代表完整合规 */
40
+ staticScan?: boolean;
41
+ /** 项目扫描的文件总数(静态扫描路径) */
42
+ filesScanned?: number;
39
43
  }
40
44
  /** 采集真实环境事实(运行时调用;测试可绕过直接注入 EnvFacts) */
41
45
  export declare function gatherEnvFacts(): EnvFacts;
@@ -103,11 +103,12 @@ export function runProjectComplianceAudit(config, root) {
103
103
  const env = gatherEnvFacts();
104
104
  // 把文件中实测到的境外端点/依赖并入 facts(按 endpointId 或 provider 去重),
105
105
  // 使数据出境项基于真实证据(含 SDK 依赖通道)
106
- const seen = new Set(env.overseas.map(o => o.endpointId || o.provider_en));
106
+ const seen = new Set(env.overseas.map(o => (o.endpointId || o.provider_en || '').toLowerCase()));
107
107
  for (const f of scan.findings) {
108
108
  if (f.kind !== 'overseas')
109
109
  continue;
110
- const key = f.endpointId || f.provider_en || '';
110
+ // 按厂商去重(不区分大小写),避免"端点命中"与"SDK依赖命中"把同一厂商列两次
111
+ const key = (f.provider_en || f.endpointId || '').toLowerCase();
111
112
  if (!key || seen.has(key))
112
113
  continue;
113
114
  seen.add(key);
@@ -127,6 +128,8 @@ export function runProjectComplianceAudit(config, root) {
127
128
  report.grade = gradeOf(report.score);
128
129
  report.projectPenalty = penalty;
129
130
  }
131
+ report.staticScan = true;
132
+ report.filesScanned = scan.filesScanned;
130
133
  return { report, scan };
131
134
  }
132
135
  const FINDING_PENALTY = { critical: 8, high: 4, medium: 1 };
@@ -40,27 +40,39 @@ export function renderHtmlReport(report, scan, locale, meta) {
40
40
  const when = report.generatedAt.slice(0, 19).replace('T', ' ');
41
41
  const S = [];
42
42
  // ===== 评分 Hero =====
43
+ // 诚实原则:静态扫描下多数控制项不可验证,不展示"优秀/A"式合规结论,
44
+ // 改以「风险发现数」为主指标,得分仅作"可观测项"的次要参考。
45
+ const findingsN = scan.findings.length;
46
+ const gradeLabel = report.staticScan ? t('可观测项', 'observable') : t(g.zh, g.en);
47
+ const verdict = report.staticScan
48
+ ? (findingsN === 0
49
+ ? { txt: t('未发现可观测风险', 'No observable risks'), c: '#16a34a', ic: '🟢' }
50
+ : { txt: t(`发现 ${findingsN} 项风险`, `${findingsN} risk(s) found`), c: '#dc2626', ic: '🔴' })
51
+ : { txt: t(g.zh, g.en), c: g.color, ic: '' };
43
52
  S.push(`
44
53
  <section class="hero">
45
54
  <div class="gauge" style="--p:${report.score};--c:${g.color}">
46
55
  <div class="gauge-in">
47
56
  <div class="gscore">${report.score}<small>/100</small></div>
48
- <div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${t(g.zh, g.en)}</div>
57
+ <div class="ggrade" style="color:${g.color}">${esc(report.grade)} · ${esc(gradeLabel)}</div>
49
58
  </div>
50
59
  </div>
51
60
  <div class="hero-side">
61
+ <div class="verdict" style="--vc:${verdict.c}">${verdict.ic} ${esc(verdict.txt)}</div>
52
62
  <div class="stat-row">
53
63
  ${stat('pass', '🟢', t('合规', 'Pass'), report.passed)}
54
64
  ${stat('warn', '🟡', t('部分', 'Partial'), report.warned)}
55
65
  ${stat('fail', '🔴', t('不合规', 'Fail'), report.failed)}
56
- ${stat('manual', '⚪', t('待确认', 'Review'), report.manual)}
66
+ ${stat('manual', '⚪', t('待核验', 'Review'), report.manual)}
57
67
  </div>
58
68
  ${report.projectPenalty ? `<div class="penalty">⚠ ${t('含项目实测风险扣分', 'Includes project-scan penalty')} <b>−${report.projectPenalty}</b></div>` : ''}
59
- <p class="hero-note">${t('得分基于本次可静态观测的项目风险。⚪ 待确认项需把 ShellWard 部署为运行时防护或人工核验。', 'Score reflects statically-observable project risk. ⚪ items need runtime deployment or manual review.')}</p>
69
+ <p class="hero-note">${report.staticScan
70
+ ? t(`⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? scan.filesScanned} 个文件,仅评估可观测风险。<b>${report.manual} 项合规控制项未验证</b>(需部署 ShellWard 运行时或人工核验)——本报告不构成完整合规结论,得分仅供参考。`, `⚠ Static scan: checked ${report.filesScanned ?? scan.filesScanned} files for observable risk only. <b>${report.manual} controls unverified</b> — not a complete compliance verdict.`)
71
+ : t('得分基于已部署运行时的合规评估。', 'Score based on deployed-runtime assessment.')}</p>
60
72
  </div>
61
73
  </section>`);
62
74
  // ===== 项目实测风险 =====
63
- S.push(sectionHead('🔍', t('项目实测风险', 'Project Scan Findings'), t(`已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限)' : ''}`, `Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''}`)));
75
+ S.push(sectionHead('🔍', t('项目实测风险', 'Project Scan Findings'), t(`已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限)' : ''} · 耗时 ${scan.durationMs ?? '?'}ms · 应用 ${scan.rulesChecked ?? '?'} 条检测规则`, `Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`)));
64
76
  if (scan.findings.length === 0) {
65
77
  S.push(`<div class="empty">🟢 ${t('未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。', 'No hardcoded secrets, PII, or overseas endpoints found in project files.')}</div>`);
66
78
  }
@@ -225,6 +237,7 @@ section,.reg{padding:0 36px}
225
237
  .gscore small{font-size:15px;font-weight:500;color:var(--faint)}
226
238
  .ggrade{font-size:14px;font-weight:700;margin-top:6px}
227
239
  .hero-side{flex:1;min-width:0}
240
+ .verdict{font-size:17px;font-weight:800;color:var(--vc);margin:0 0 12px}
228
241
  .stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
229
242
  .stat{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;text-align:center}
230
243
  .stat .sn{font-size:22px;font-weight:800;line-height:1}
@@ -18,6 +18,10 @@ export interface ProjectScanResult {
18
18
  truncated: boolean;
19
19
  findings: ProjectFinding[];
20
20
  counts: Record<FindingKind, number>;
21
+ /** 扫描耗时(ms) —— 透明度:让用户看到确实在干活 */
22
+ durationMs?: number;
23
+ /** 本次应用的检测规则总数(端点 + SDK 依赖 + 密钥/PII 模式) */
24
+ rulesChecked?: number;
21
25
  }
22
26
  /** 扫描项目目录,返回真实风险发现 */
23
27
  export declare function scanProject(root: string): ProjectScanResult;
Binary file
@@ -31,21 +31,39 @@ export function renderComplianceReport(report, locale) {
31
31
  const bar = scoreBar(report.score);
32
32
  L.push(zh ? '## 总评' : '## Overall');
33
33
  L.push('');
34
- L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`);
35
- L.push('');
36
- L.push('```');
37
- L.push(`${bar} ${report.score}/100 [${report.grade}]`);
38
- L.push('```');
34
+ // 诚实原则:静态扫描下不报"优秀/A"式合规结论,以风险发现数为主指标
35
+ if (report.staticScan) {
36
+ const fn = report.failed + (report.projectPenalty ? 1 : 0);
37
+ L.push(zh
38
+ ? `**项目实测风险: ${report.failed === 0 && !report.projectPenalty ? '🟢 未发现可观测风险' : '🔴 发现风险,详见下方'}**`
39
+ : `**Observable risk: ${report.failed === 0 && !report.projectPenalty ? '🟢 none found' : '🔴 see below'}**`);
40
+ L.push('');
41
+ L.push(`${bar} ${report.score}/100 ${zh ? '(可观测合规项,仅供参考)' : '(observable controls only)'}`);
42
+ void fn;
43
+ }
44
+ else {
45
+ L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`);
46
+ L.push('');
47
+ L.push('```');
48
+ L.push(`${bar} ${report.score}/100 [${report.grade}]`);
49
+ L.push('```');
50
+ }
39
51
  L.push('');
40
52
  L.push(zh
41
- ? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待确认 ${report.manual} (共 ${report.total} 项)`
42
- : `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Manual ${report.manual} (${report.total} controls)`);
53
+ ? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待核验 ${report.manual} (共 ${report.total} 项)`
54
+ : `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Review ${report.manual} (${report.total} controls)`);
43
55
  if (report.projectPenalty && report.projectPenalty > 0) {
44
56
  L.push('');
45
57
  L.push(zh
46
58
  ? `> 含项目实测风险扣分 **-${report.projectPenalty}**(见下方「项目实测风险」)`
47
59
  : `> Includes **-${report.projectPenalty}** from project scan findings (see Project Scan Findings)`);
48
60
  }
61
+ if (report.staticScan) {
62
+ L.push('');
63
+ L.push(zh
64
+ ? `> ⚠ 本次为静态扫描:已检查 ${report.filesScanned ?? '?'} 个文件,仅评估可观测风险;**${report.manual} 项合规控制项未验证**(需部署 ShellWard 运行时或人工核验)。本报告不构成完整合规结论。`
65
+ : `> ⚠ Static scan: ${report.filesScanned ?? '?'} files checked; **${report.manual} controls unverified**. Not a complete compliance verdict.`);
66
+ }
49
67
  L.push('');
50
68
  // ===== 优先整改(fail 项,按严重度) =====
51
69
  const fails = report.results
@@ -108,8 +126,8 @@ export function renderProjectFindings(scan, locale) {
108
126
  L.push(zh ? '## 🔍 项目实测风险' : '## 🔍 Project Scan Findings');
109
127
  L.push('');
110
128
  L.push(zh
111
- ? `> 已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限,部分未扫)' : ''}`
112
- : `> Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached, partial)' : ''}`);
129
+ ? `> 已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限)' : ''} · 耗时 ${scan.durationMs ?? '?'}ms · 应用 ${scan.rulesChecked ?? '?'} 条检测规则`
130
+ : `> Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`);
113
131
  L.push('');
114
132
  if (scan.findings.length === 0) {
115
133
  L.push(zh ? '🟢 未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。' : '🟢 No hardcoded secrets, PII exposure, or overseas endpoints found in project files.');
@@ -21,8 +21,8 @@ import { runProjectComplianceAudit } from '../compliance/audit.js';
21
21
  import { renderHtmlReport } from '../compliance/html-report.js';
22
22
  import { DEFAULT_CONFIG, resolveLocale } from '../types.js';
23
23
  const REPO_RE = /^https:\/\/(github\.com|gitlab\.com|gitee\.com|bitbucket\.org)\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?\/?$/;
24
- const CLONE_TIMEOUT_MS = 30_000;
25
- const MAX_CONCURRENT = 2;
24
+ const CLONE_TIMEOUT_MS = 60_000;
25
+ const MAX_CONCURRENT = 4;
26
26
  /** 校验仓库 URL:仅允许白名单代码托管域名,拒绝带凭据/异常字符 */
27
27
  export function validateRepoUrl(input) {
28
28
  const url = (input || '').trim();
@@ -101,7 +101,9 @@ async function handleRepo(res, repo, locale, inc, dec) {
101
101
  send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }));
102
102
  }
103
103
  catch (e) {
104
- send(res, 502, 'text/html', errorPage('克隆/扫描失败:' + esc(e?.message || String(e)) + '。请确认是可公开访问的仓库。'));
104
+ const msg = esc(e?.message || String(e));
105
+ send(res, 502, 'text/html', errorPage(`克隆/扫描失败:${msg}。<br><br>可能原因:仓库过大(克隆超时 60s)、私有仓库、或地址有误。<br>` +
106
+ `<b>大仓库 / 私有代码请用本地客户端</b>(选文件夹、不上传):<code>npx shellward web --local</code>,或命令行 <code>npx shellward scan</code>。`));
105
107
  }
106
108
  finally {
107
109
  dec();
@@ -214,11 +216,11 @@ function send(res, code, type, body) {
214
216
  }
215
217
  function formPage(local) {
216
218
  const urlForm = `
217
- <form action="/scan" method="get">
219
+ <form action="/scan" method="get" onsubmit="var b=this.querySelector('button');b.disabled=true;b.textContent='扫描中…(大仓库需 10–60 秒,请勿重复点击)';">
218
220
  <label>${local ? '② ' : ''}公开仓库地址</label>
219
221
  <input name="repo" placeholder="https://github.com/owner/repo"${local ? '' : ' autofocus'}>
220
222
  <button type="submit">${local ? '体检该仓库 →' : '开始体检 →'}</button>
221
- <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。${local ? '' : '<b>私有/敏感代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
223
+ <p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。大仓库可能超时——${local ? '此时改用上方「选择文件夹」更稳。' : '<b>大仓库 / 私有代码请用本地客户端或 CLI</b>:<code>npx shellward web --local</code> / <code>npx shellward scan</code>(不上传)。'}</p>
222
224
  </form>`;
223
225
  const uploadForm = local ? `
224
226
  <form id="dirform">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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": [
@@ -57,6 +57,10 @@ export interface ComplianceReport {
57
57
  generatedAt: string
58
58
  /** 项目实测风险造成的扣分(仅项目体检路径);0 表示纯控制项评分 */
59
59
  projectPenalty?: number
60
+ /** 是否为静态扫描(未部署运行时):此时多数控制项不可验证,得分不代表完整合规 */
61
+ staticScan?: boolean
62
+ /** 项目扫描的文件总数(静态扫描路径) */
63
+ filesScanned?: number
60
64
  }
61
65
 
62
66
  /** 层能力映射:控制项 id → 必须启用的层(全部启用才 pass,部分启用 warn,全关 fail) */
@@ -167,10 +171,11 @@ export function runProjectComplianceAudit(config: ShellWardConfig, root: string)
167
171
 
168
172
  // 把文件中实测到的境外端点/依赖并入 facts(按 endpointId 或 provider 去重),
169
173
  // 使数据出境项基于真实证据(含 SDK 依赖通道)
170
- const seen = new Set(env.overseas.map(o => o.endpointId || o.provider_en))
174
+ const seen = new Set(env.overseas.map(o => (o.endpointId || o.provider_en || '').toLowerCase()))
171
175
  for (const f of scan.findings) {
172
176
  if (f.kind !== 'overseas') continue
173
- const key = f.endpointId || f.provider_en || ''
177
+ // 按厂商去重(不区分大小写),避免"端点命中"与"SDK依赖命中"把同一厂商列两次
178
+ const key = (f.provider_en || f.endpointId || '').toLowerCase()
174
179
  if (!key || seen.has(key)) continue
175
180
  seen.add(key)
176
181
  env.overseas.push({
@@ -191,6 +196,8 @@ export function runProjectComplianceAudit(config: ShellWardConfig, root: string)
191
196
  report.grade = gradeOf(report.score)
192
197
  report.projectPenalty = penalty
193
198
  }
199
+ report.staticScan = true
200
+ report.filesScanned = scan.filesScanned
194
201
 
195
202
  return { report, scan }
196
203
  }
@@ -60,32 +60,44 @@ export function renderHtmlReport(
60
60
  const S: string[] = []
61
61
 
62
62
  // ===== 评分 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
+
63
73
  S.push(`
64
74
  <section class="hero">
65
75
  <div class="gauge" style="--p:${report.score};--c:${g.color}">
66
76
  <div class="gauge-in">
67
77
  <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>
69
79
  </div>
70
80
  </div>
71
81
  <div class="hero-side">
82
+ <div class="verdict" style="--vc:${verdict.c}">${verdict.ic} ${esc(verdict.txt)}</div>
72
83
  <div class="stat-row">
73
84
  ${stat('pass', '🟢', t('合规', 'Pass'), report.passed)}
74
85
  ${stat('warn', '🟡', t('部分', 'Partial'), report.warned)}
75
86
  ${stat('fail', '🔴', t('不合规', 'Fail'), report.failed)}
76
- ${stat('manual', '⚪', t('待确认', 'Review'), report.manual)}
87
+ ${stat('manual', '⚪', t('待核验', 'Review'), report.manual)}
77
88
  </div>
78
89
  ${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>
82
94
  </div>
83
95
  </section>`)
84
96
 
85
97
  // ===== 项目实测风险 =====
86
98
  S.push(sectionHead('🔍', t('项目实测风险', 'Project Scan Findings'),
87
- t(`已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限)' : ''}`,
88
- `Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''}`)))
99
+ t(`已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限)' : ''} · 耗时 ${scan.durationMs ?? '?'}ms · 应用 ${scan.rulesChecked ?? '?'} 条检测规则`,
100
+ `Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`)))
89
101
 
90
102
  if (scan.findings.length === 0) {
91
103
  S.push(`<div class="empty">🟢 ${t('未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。',
@@ -263,6 +275,7 @@ section,.reg{padding:0 36px}
263
275
  .gscore small{font-size:15px;font-weight:500;color:var(--faint)}
264
276
  .ggrade{font-size:14px;font-weight:700;margin-top:6px}
265
277
  .hero-side{flex:1;min-width:0}
278
+ .verdict{font-size:17px;font-weight:800;color:var(--vc);margin:0 0 12px}
266
279
  .stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
267
280
  .stat{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;text-align:center}
268
281
  .stat .sn{font-size:22px;font-weight:800;line-height:1}
Binary file
@@ -41,21 +41,38 @@ export function renderComplianceReport(report: ComplianceReport, locale: 'zh' |
41
41
  const bar = scoreBar(report.score)
42
42
  L.push(zh ? '## 总评' : '## Overall')
43
43
  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
+ }
49
60
  L.push('')
50
61
  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)`)
53
64
  if (report.projectPenalty && report.projectPenalty > 0) {
54
65
  L.push('')
55
66
  L.push(zh
56
67
  ? `> 含项目实测风险扣分 **-${report.projectPenalty}**(见下方「项目实测风险」)`
57
68
  : `> Includes **-${report.projectPenalty}** from project scan findings (see Project Scan Findings)`)
58
69
  }
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
+ }
59
76
  L.push('')
60
77
 
61
78
  // ===== 优先整改(fail 项,按严重度) =====
@@ -124,8 +141,8 @@ export function renderProjectFindings(scan: ProjectScanResult, locale: 'zh' | 'e
124
141
  L.push(zh ? '## 🔍 项目实测风险' : '## 🔍 Project Scan Findings')
125
142
  L.push('')
126
143
  L.push(zh
127
- ? `> 已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限,部分未扫)' : ''}`
128
- : `> Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached, partial)' : ''}`)
144
+ ? `> 已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限)' : ''} · 耗时 ${scan.durationMs ?? '?'}ms · 应用 ${scan.rulesChecked ?? '?'} 条检测规则`
145
+ : `> Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached)' : ''} · ${scan.durationMs ?? '?'}ms · ${scan.rulesChecked ?? '?'} detection rules`)
129
146
  L.push('')
130
147
 
131
148
  if (scan.findings.length === 0) {
@@ -23,8 +23,8 @@ import { renderHtmlReport } from '../compliance/html-report.js'
23
23
  import { DEFAULT_CONFIG, resolveLocale } from '../types.js'
24
24
 
25
25
  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
28
28
 
29
29
  export interface WebServerOptions {
30
30
  port: number
@@ -105,7 +105,10 @@ async function handleRepo(res: any, repo: string, locale: 'zh' | 'en', inc: () =
105
105
  const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir)
106
106
  send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }))
107
107
  } 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>。`))
109
112
  } finally {
110
113
  dec()
111
114
  try { rmSync(dir, { recursive: true, force: true }) } catch { /* ignore */ }
@@ -197,11 +200,11 @@ function send(res: any, code: number, type: string, body: string) {
197
200
 
198
201
  function formPage(local: boolean): string {
199
202
  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 秒,请勿重复点击)';">
201
204
  <label>${local ? '② ' : ''}公开仓库地址</label>
202
205
  <input name="repo" placeholder="https://github.com/owner/repo"${local ? '' : ' autofocus'}>
203
206
  <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>
205
208
  </form>`
206
209
 
207
210
  const uploadForm = local ? `