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 +1 -1
- package/dist/compliance/audit.d.ts +4 -0
- package/dist/compliance/audit.js +5 -2
- package/dist/compliance/html-report.js +17 -4
- package/dist/compliance/project-scan.d.ts +4 -0
- package/dist/compliance/project-scan.js +0 -0
- package/dist/compliance/report.js +27 -9
- package/dist/web/scan-server.js +7 -5
- package/package.json +1 -1
- package/src/compliance/audit.ts +9 -2
- package/src/compliance/html-report.ts +20 -7
- package/src/compliance/project-scan.ts +0 -0
- package/src/compliance/report.ts +26 -9
- package/src/web/scan-server.ts +8 -5
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/**
|
|
@@ -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;
|
package/dist/compliance/audit.js
CHANGED
|
@@ -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
|
-
|
|
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)} · ${
|
|
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('
|
|
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">${
|
|
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 ? '(已达上限)' : ''}
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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} | ⚪
|
|
42
|
-
: `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪
|
|
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
|
|
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.');
|
package/dist/web/scan-server.js
CHANGED
|
@@ -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 =
|
|
25
|
-
const MAX_CONCURRENT =
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": [
|
package/src/compliance/audit.ts
CHANGED
|
@@ -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
|
-
|
|
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)} · ${
|
|
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('
|
|
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">${
|
|
80
|
-
|
|
81
|
-
|
|
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
|
package/src/compliance/report.ts
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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} | ⚪
|
|
52
|
-
: `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪
|
|
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
|
|
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) {
|
package/src/web/scan-server.ts
CHANGED
|
@@ -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 =
|
|
27
|
-
const MAX_CONCURRENT =
|
|
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
|
-
|
|
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
|
|
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 ? `
|