shellward 0.7.1 → 0.7.2
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 +2 -0
- package/dist/compliance/html-report.js +16 -3
- package/dist/compliance/project-scan.js +0 -0
- package/dist/compliance/report.js +25 -7
- package/dist/web/scan-server.js +7 -5
- package/package.json +1 -1
- package/src/compliance/audit.ts +6 -0
- package/src/compliance/html-report.ts +18 -5
- package/src/compliance/project-scan.ts +0 -0
- package/src/compliance/report.ts +24 -7
- 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
|
@@ -127,6 +127,8 @@ export function runProjectComplianceAudit(config, root) {
|
|
|
127
127
|
report.grade = gradeOf(report.score);
|
|
128
128
|
report.projectPenalty = penalty;
|
|
129
129
|
}
|
|
130
|
+
report.staticScan = true;
|
|
131
|
+
report.filesScanned = scan.filesScanned;
|
|
130
132
|
return { report, scan };
|
|
131
133
|
}
|
|
132
134
|
const FINDING_PENALTY = { critical: 8, high: 4, medium: 1 };
|
|
@@ -40,23 +40,35 @@ 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
|
// ===== 项目实测风险 =====
|
|
@@ -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}
|
|
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
|
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.2",
|
|
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) */
|
|
@@ -191,6 +195,8 @@ export function runProjectComplianceAudit(config: ShellWardConfig, root: string)
|
|
|
191
195
|
report.grade = gradeOf(report.score)
|
|
192
196
|
report.projectPenalty = penalty
|
|
193
197
|
}
|
|
198
|
+
report.staticScan = true
|
|
199
|
+
report.filesScanned = scan.filesScanned
|
|
194
200
|
|
|
195
201
|
return { report, scan }
|
|
196
202
|
}
|
|
@@ -60,25 +60,37 @@ 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
|
|
|
@@ -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 项,按严重度) =====
|
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 ? `
|