shellward 0.6.8 → 0.7.0
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 +5 -1
- package/dist/cli.js +11 -0
- package/dist/compliance/html-report.js +222 -84
- package/dist/web/scan-server.d.ts +14 -0
- package/dist/web/scan-server.js +193 -0
- package/package.json +1 -1
- package/src/cli.ts +12 -0
- package/src/compliance/html-report.ts +236 -85
- package/src/web/scan-server.ts +201 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/web/scan-server.ts — ShellWard 合规扫描 web 服务(零依赖)
|
|
2
|
+
//
|
|
3
|
+
// 双模式,一份代码两用:
|
|
4
|
+
// 1) 公网模式(部署):贴「公开仓库 URL」或访问 /scan?repo=URL → 浅克隆 + 扫描 + 出报告
|
|
5
|
+
// 公开仓库的代码本就公开,服务端扫描不涉及数据出境;私有代码引导用本地 CLI。
|
|
6
|
+
// 2) 本地模式(shellward web --local,仅 127.0.0.1):填「本地路径」扫描,私有代码不上传
|
|
7
|
+
// —— 这就是「客户端」体验(浏览器 GUI、不用命令行),但零 Electron 包袱。
|
|
8
|
+
//
|
|
9
|
+
// 安全加固:
|
|
10
|
+
// - 仓库 URL 域名白名单(github/gitlab/gitee...),严格正则,拒带凭据的 URL
|
|
11
|
+
// - 浅克隆 --depth 1 --single-branch,GIT_TERMINAL_PROMPT=0(不卡在私有库鉴权),30s 超时
|
|
12
|
+
// - 临时目录隔离,用完即删;扫描器只读文件、绝不执行仓库代码
|
|
13
|
+
// - 本地路径扫描仅在 --local 模式开放(公网模式拒绝 path 参数,防止扫服务器硬盘)
|
|
14
|
+
// - 并发上限,防滥用
|
|
15
|
+
import { createServer } from 'http';
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
import { mkdtempSync, rmSync, existsSync, statSync } from 'fs';
|
|
18
|
+
import { tmpdir } from 'os';
|
|
19
|
+
import { join, resolve } from 'path';
|
|
20
|
+
import { runProjectComplianceAudit } from '../compliance/audit.js';
|
|
21
|
+
import { renderHtmlReport } from '../compliance/html-report.js';
|
|
22
|
+
import { DEFAULT_CONFIG, resolveLocale } from '../types.js';
|
|
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;
|
|
26
|
+
/** 校验仓库 URL:仅允许白名单代码托管域名,拒绝带凭据/异常字符 */
|
|
27
|
+
export function validateRepoUrl(input) {
|
|
28
|
+
const url = (input || '').trim();
|
|
29
|
+
if (!url)
|
|
30
|
+
return { ok: false, reason: '请输入仓库地址' };
|
|
31
|
+
if (url.includes('@') || /\s/.test(url))
|
|
32
|
+
return { ok: false, reason: '地址含非法字符' };
|
|
33
|
+
if (!REPO_RE.test(url))
|
|
34
|
+
return { ok: false, reason: '仅支持 github.com / gitlab.com / gitee.com / bitbucket.org 的公开仓库 URL' };
|
|
35
|
+
return { ok: true, url };
|
|
36
|
+
}
|
|
37
|
+
export function startWebServer(opts) {
|
|
38
|
+
const locale = resolveLocale(DEFAULT_CONFIG);
|
|
39
|
+
const host = opts.local ? '127.0.0.1' : '0.0.0.0';
|
|
40
|
+
let active = 0;
|
|
41
|
+
const server = createServer(async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const u = new URL(req.url || '/', `http://localhost:${opts.port}`);
|
|
44
|
+
if (u.pathname === '/' || u.pathname === '') {
|
|
45
|
+
return send(res, 200, 'text/html', formPage(!!opts.local));
|
|
46
|
+
}
|
|
47
|
+
if (u.pathname === '/scan') {
|
|
48
|
+
if (active >= MAX_CONCURRENT) {
|
|
49
|
+
return send(res, 503, 'text/html', errorPage('服务繁忙,请稍后再试(并发上限)'));
|
|
50
|
+
}
|
|
51
|
+
const repo = u.searchParams.get('repo');
|
|
52
|
+
const path = u.searchParams.get('path');
|
|
53
|
+
// 本地路径扫描:仅本地模式开放
|
|
54
|
+
if (path) {
|
|
55
|
+
if (!opts.local)
|
|
56
|
+
return send(res, 403, 'text/html', errorPage('公网模式不支持本地路径扫描;请用「公开仓库 URL」,私有代码请用本地 CLI:npx shellward scan'));
|
|
57
|
+
return await handleLocal(res, path, locale, () => { active++; }, () => { active--; });
|
|
58
|
+
}
|
|
59
|
+
if (repo) {
|
|
60
|
+
return await handleRepo(res, repo, locale, () => { active++; }, () => { active--; });
|
|
61
|
+
}
|
|
62
|
+
return send(res, 400, 'text/html', errorPage('缺少参数:repo(仓库 URL)' + (opts.local ? ' 或 path(本地路径)' : '')));
|
|
63
|
+
}
|
|
64
|
+
send(res, 404, 'text/html', errorPage('页面不存在'));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
send(res, 500, 'text/html', errorPage('内部错误:' + esc(e?.message || String(e))));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
server.on('error', (e) => {
|
|
71
|
+
console.error(`[ShellWard web] 启动失败: ${e?.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
server.listen(opts.port, host, () => {
|
|
75
|
+
const url = `http://localhost:${opts.port}`;
|
|
76
|
+
if (opts.local) {
|
|
77
|
+
console.log(`🌐 ShellWard 本地合规扫描(客户端模式): ${url}\n 填本地路径即可扫描,私有代码不上传、不出本机。Ctrl+C 退出。`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log(`🌐 ShellWard 公开仓库合规扫描: ${url} (监听 ${host}:${opts.port})\n 贴公开仓库 URL 即可体检。私有代码请用本地 CLI: npx shellward scan`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function handleRepo(res, repo, locale, inc, dec) {
|
|
85
|
+
const v = validateRepoUrl(repo);
|
|
86
|
+
if (!v.ok)
|
|
87
|
+
return send(res, 400, 'text/html', errorPage(v.reason));
|
|
88
|
+
const dir = mkdtempSync(join(tmpdir(), 'sw-web-'));
|
|
89
|
+
inc();
|
|
90
|
+
try {
|
|
91
|
+
await cloneRepo(v.url, dir);
|
|
92
|
+
const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, dir);
|
|
93
|
+
send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root: v.url }));
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
send(res, 502, 'text/html', errorPage('克隆/扫描失败:' + esc(e?.message || String(e)) + '。请确认是可公开访问的仓库。'));
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
dec();
|
|
100
|
+
try {
|
|
101
|
+
rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
catch { /* ignore */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function handleLocal(res, path, locale, inc, dec) {
|
|
107
|
+
const root = resolve(path);
|
|
108
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
109
|
+
return send(res, 400, 'text/html', errorPage('路径不存在或不是目录:' + esc(root)));
|
|
110
|
+
}
|
|
111
|
+
inc();
|
|
112
|
+
try {
|
|
113
|
+
const { report, scan } = runProjectComplianceAudit(DEFAULT_CONFIG, root);
|
|
114
|
+
send(res, 200, 'text/html', renderHtmlReport(report, scan, locale, { root }));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
dec();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** 浅克隆公开仓库到临时目录(不鉴权、超时、不执行任何仓库代码) */
|
|
121
|
+
function cloneRepo(url, dir) {
|
|
122
|
+
return new Promise((res, rej) => {
|
|
123
|
+
const p = spawn('git', ['clone', '--depth', '1', '--single-branch', '--no-tags', url, dir], {
|
|
124
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'true' },
|
|
125
|
+
timeout: CLONE_TIMEOUT_MS,
|
|
126
|
+
stdio: 'ignore',
|
|
127
|
+
});
|
|
128
|
+
p.on('error', rej);
|
|
129
|
+
p.on('close', (code, signal) => {
|
|
130
|
+
if (signal)
|
|
131
|
+
return rej(new Error('克隆超时'));
|
|
132
|
+
code === 0 ? res() : rej(new Error('克隆失败 (git exit ' + code + ')'));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// ===== 页面 =====
|
|
137
|
+
function send(res, code, type, body) {
|
|
138
|
+
res.writeHead(code, { 'Content-Type': type + '; charset=utf-8', 'X-Content-Type-Options': 'nosniff' });
|
|
139
|
+
res.end(body);
|
|
140
|
+
}
|
|
141
|
+
function formPage(local) {
|
|
142
|
+
const field = local
|
|
143
|
+
? `<label>本地项目路径</label>
|
|
144
|
+
<input name="path" placeholder="/Users/you/your-ai-project" autofocus>
|
|
145
|
+
<p class="hint">本地模式:代码不上传、不出本机(客户端体验)。</p>`
|
|
146
|
+
: `<label>公开仓库地址</label>
|
|
147
|
+
<input name="repo" placeholder="https://github.com/owner/repo" autofocus>
|
|
148
|
+
<p class="hint">仅支持公开仓库(GitHub / GitLab / Gitee / Bitbucket)。<b>私有/敏感代码请用本地 CLI</b>:<code>npx shellward scan</code>(不上传)。</p>`;
|
|
149
|
+
return page('ShellWard 合规体检', `
|
|
150
|
+
<div class="hero">
|
|
151
|
+
<div class="logo">🛡️ Shell<span>Ward</span> 合规网关</div>
|
|
152
|
+
<h1>AI 应用合规体检</h1>
|
|
153
|
+
<p class="sub">${local ? '填本地路径' : '贴公开仓库链接'},30 秒查出数据出境 / 硬编码密钥 / 个人信息暴露等中国合规红线。</p>
|
|
154
|
+
<form action="/scan" method="get">
|
|
155
|
+
${field}
|
|
156
|
+
<button type="submit">开始体检 →</button>
|
|
157
|
+
</form>
|
|
158
|
+
<p class="foot">网安法 2026 · PIPL · 等保2.0 · 数据出境 · AI标识 | 零依赖 · 开源 ·
|
|
159
|
+
<a href="https://github.com/jnMetaCode/shellward">GitHub ⭐</a></p>
|
|
160
|
+
</div>`);
|
|
161
|
+
}
|
|
162
|
+
function errorPage(msg) {
|
|
163
|
+
return page('出错了', `<div class="hero"><div class="logo">🛡️ Shell<span>Ward</span></div>
|
|
164
|
+
<h1>⚠️ 无法完成</h1><p class="sub">${esc(msg)}</p>
|
|
165
|
+
<p><a class="back" href="/">← 返回重试</a></p></div>`);
|
|
166
|
+
}
|
|
167
|
+
function page(title, body) {
|
|
168
|
+
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
|
|
169
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title)}</title>
|
|
170
|
+
<style>
|
|
171
|
+
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;
|
|
172
|
+
background:linear-gradient(135deg,#eef1f6,#e2e8f0);color:#0f172a;
|
|
173
|
+
font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}
|
|
174
|
+
.hero{background:#fff;max-width:560px;width:92%;margin:40px;padding:40px;border-radius:18px;
|
|
175
|
+
box-shadow:0 12px 40px rgba(15,23,42,.12);text-align:center}
|
|
176
|
+
.logo{font-weight:800;font-size:15px}.logo span{color:#cb0000}
|
|
177
|
+
h1{font-size:30px;margin:14px 0 8px;letter-spacing:-.5px}
|
|
178
|
+
.sub{color:#64748b;margin:0 0 26px}
|
|
179
|
+
form{display:flex;flex-direction:column;gap:10px;text-align:left}
|
|
180
|
+
label{font-size:13px;font-weight:600;color:#475569}
|
|
181
|
+
input{padding:14px 16px;border:1px solid #cbd5e1;border-radius:10px;font-size:16px;width:100%}
|
|
182
|
+
input:focus{outline:none;border-color:#cb0000;box-shadow:0 0 0 3px rgba(203,0,0,.12)}
|
|
183
|
+
.hint{font-size:12.5px;color:#64748b;margin:2px 0 6px}
|
|
184
|
+
.hint code{background:#f1f5f9;padding:1px 6px;border-radius:5px}
|
|
185
|
+
button{background:#cb0000;color:#fff;border:0;border-radius:10px;padding:14px;font-size:16px;
|
|
186
|
+
font-weight:700;cursor:pointer;margin-top:4px}button:hover{background:#a80000}
|
|
187
|
+
.foot{margin:24px 0 0;font-size:12.5px;color:#94a3b8}.foot a,.back{color:#cb0000;text-decoration:none}
|
|
188
|
+
.back{font-weight:600}
|
|
189
|
+
</style></head><body>${body}</body></html>`;
|
|
190
|
+
}
|
|
191
|
+
function esc(s) {
|
|
192
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shellward",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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/cli.ts
CHANGED
|
@@ -36,6 +36,14 @@ async function main() {
|
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (cmd === 'web') {
|
|
40
|
+
const { startWebServer } = await import('./web/scan-server.js')
|
|
41
|
+
const local = argv.includes('--local')
|
|
42
|
+
const portArg = flagValue(argv, '--port') || argv.slice(1).find(a => /^\d+$/.test(a))
|
|
43
|
+
startWebServer({ port: Number(portArg) || 8080, local })
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
if (cmd === 'scan') {
|
|
40
48
|
runScan(argv.slice(1))
|
|
41
49
|
return
|
|
@@ -189,6 +197,8 @@ Usage:
|
|
|
189
197
|
shellward scan --html f Export a self-contained HTML report (print to PDF)
|
|
190
198
|
shellward scan --open Scan and open the report in your browser (local)
|
|
191
199
|
shellward scan --serve Scan and serve the report at http://localhost (local)
|
|
200
|
+
shellward web [port] Web scanner for public repo URLs (deploy this)
|
|
201
|
+
shellward web --local Local web GUI: scan a local path (private, no upload)
|
|
192
202
|
shellward mcp Start MCP server (stdio)
|
|
193
203
|
shellward --help
|
|
194
204
|
|
|
@@ -205,6 +215,8 @@ PII in files, .env permissions. Maps to CSL / PIPL / MLPS / cross-border / label
|
|
|
205
215
|
shellward scan --html 文件 导出自包含 HTML 报告(浏览器可打印成 PDF)
|
|
206
216
|
shellward scan --open 扫描并在浏览器打开报告(本地,方便看)
|
|
207
217
|
shellward scan --serve 扫描并在 http://localhost 提供报告(本地服务)
|
|
218
|
+
shellward web [端口] 公开仓库 web 扫描器(贴 URL 体检,用于部署)
|
|
219
|
+
shellward web --local 本地 web GUI:填本地路径扫描(私有、不上传,客户端体验)
|
|
208
220
|
shellward mcp 启动 MCP 服务器(stdio)
|
|
209
221
|
shellward --help
|
|
210
222
|
|