shellward 0.7.12 → 0.7.14

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-303%20passing-brightgreen)](#performance)
11
+ [![tests](https://img.shields.io/badge/tests-315%20passing-brightgreen)](#performance)
12
12
  [![deps](https://img.shields.io/badge/dependencies-0-brightgreen)](#performance)
13
13
 
14
14
  **🌐 官网: https://jnmetacode.github.io/shellward/**
package/dist/cli.js CHANGED
@@ -17,6 +17,7 @@ import { ShellWard } from './core/engine.js';
17
17
  import { runProjectComplianceAudit } from './compliance/audit.js';
18
18
  import { renderComplianceReport, renderProjectFindings } from './compliance/report.js';
19
19
  import { renderHtmlReport } from './compliance/html-report.js';
20
+ import { runInit } from './init.js';
20
21
  import { resolveLocale } from './types.js';
21
22
  const argv = process.argv.slice(2);
22
23
  const wantsHelp = argv.includes('--help') || argv.includes('-h') || argv[0] === 'help';
@@ -31,6 +32,10 @@ async function main() {
31
32
  await import('./mcp-server.js');
32
33
  return;
33
34
  }
35
+ if (cmd === 'init') {
36
+ runInitCommand(argv.includes('--dry-run'));
37
+ return;
38
+ }
34
39
  if (cmd === 'web') {
35
40
  const { startWebServer } = await import('./web/scan-server.js');
36
41
  const local = argv.includes('--local');
@@ -151,6 +156,33 @@ function runScan(args) {
151
156
  process.exit(1);
152
157
  }
153
158
  }
159
+ /** `shellward init`:把 ShellWard 接入已安装 AI 工具的 MCP 配置(运行时防护) */
160
+ function runInitCommand(dryRun) {
161
+ const out = runInit({ dryRun });
162
+ console.log('\n🔌 ShellWard 接入 AI 工具运行时防护' + (dryRun ? '(预览,不写入)' : '') + '\n');
163
+ const ICON = { added: '✅', updated: '✅', unchanged: '✔️', skipped: '·', error: '⚠️' };
164
+ let touched = 0;
165
+ for (const o of out) {
166
+ const label = { added: '已接入', updated: '已更新', unchanged: '已接入(无变化)', skipped: '跳过', error: '失败' }[o.result];
167
+ if (o.result === 'added' || o.result === 'updated')
168
+ touched++;
169
+ console.log(` ${ICON[o.result] || '·'} ${o.name} — ${label}${o.detail ? ':' + o.detail : ''}`);
170
+ if (o.result !== 'skipped')
171
+ console.log(` ${o.path}`);
172
+ }
173
+ console.log('');
174
+ if (dryRun) {
175
+ console.log('这是预览。去掉 --dry-run 实际接入。');
176
+ }
177
+ else if (touched > 0) {
178
+ console.log('✅ 已接入。请重启对应的 AI 工具,ShellWard 即作为运行时防护生效(拦注入/外泄/危险命令)。');
179
+ console.log(' 验证:在工具里问"调用 shellward 的 security_status"。原配置已备份为 *.shellward.bak。');
180
+ }
181
+ else {
182
+ console.log('未发现可接入的已安装 AI 工具配置。也可手动加 MCP:');
183
+ console.log(' {"mcpServers":{"shellward":{"command":"npx","args":["-y","-p","shellward","shellward-mcp"]}}}');
184
+ }
185
+ }
154
186
  /** 跨平台在默认浏览器打开 URL 或本地文件(失败静默,不影响主流程) */
155
187
  function openBrowser(target) {
156
188
  const cmd = process.platform === 'darwin' ? 'open'
@@ -187,6 +219,7 @@ Usage:
187
219
  shellward scan --serve Scan and serve the report at http://localhost (local)
188
220
  shellward web [port] Web scanner for public repo URLs (deploy this)
189
221
  shellward web --local Local web GUI: scan a local path (private, no upload)
222
+ shellward init Install ShellWard into your AI tools (MCP runtime guard)
190
223
  shellward mcp Start MCP server (stdio)
191
224
  shellward --help
192
225
 
@@ -206,6 +239,7 @@ PII in files, .env permissions. Maps to CSL / PIPL / MLPS / cross-border / label
206
239
  shellward scan --serve 扫描并在 http://localhost 提供报告(本地服务)
207
240
  shellward web [端口] 公开仓库 web 扫描器(贴 URL 体检,用于部署)
208
241
  shellward web --local 本地 web GUI:填本地路径扫描(私有、不上传,客户端体验)
242
+ shellward init 一键接入你的 AI 工具(MCP 运行时防护,--dry-run 预览)
209
243
  shellward mcp 启动 MCP 服务器(stdio)
210
244
  shellward --help
211
245
 
@@ -236,9 +236,10 @@ function checkEnv(c, env) {
236
236
  if (env.overseas.length > 0) {
237
237
  const names = env.overseas.map(o => o.provider_zh).join(', ');
238
238
  const namesEn = env.overseas.map(o => o.provider_en).join(', ');
239
- return mk(c, 'fail', `检测到境外大模型端点配置: ${names} — 若向其发送个人信息/重要数据即构成数据出境,须走合规路径`, `Overseas LLM endpoint(s) configured: ${namesEn} — sending PI/important data = cross-border export`);
239
+ // 检出境外调用是"事实",是否违规取决于是否发送 PII/重要数据 标"需评估"而非"不合规"
240
+ return mk(c, 'warn', `检测到境外大模型调用: ${names}。是否违规取决于发送的数据:涉及个人信息/重要数据需走合规路径或改用境内模型;仅非个人/非重要数据通常可接受。`, `Overseas LLM detected: ${namesEn}. Compliance depends on the data sent — PI/important data needs a compliant path; non-personal data is usually fine.`);
240
241
  }
241
- return mk(c, 'pass', '未检测到境外大模型端点配置', 'No overseas LLM endpoint detected');
242
+ return mk(c, 'pass', '未检测到境外大模型调用', 'No overseas LLM detected');
242
243
  }
243
244
  return mk(c, 'manual', '需人工确认', 'Manual check required');
244
245
  }
@@ -14,7 +14,7 @@ const STATUS = {
14
14
  manual: { zh: '待确认', en: 'Review', cls: 'manual' },
15
15
  };
16
16
  const KIND = {
17
- overseas: { zh: '数据出境风险', en: 'Data export risk', icon: '🌐' },
17
+ overseas: { zh: '境外大模型调用(需评估出境)', en: 'Overseas LLM (assess export)', icon: '🌐' },
18
18
  secret: { zh: '硬编码密钥', en: 'Hardcoded secret', icon: '🔑' },
19
19
  pii: { zh: '个人信息暴露', en: 'PII exposure', icon: '🪪' },
20
20
  'env-perm': { zh: '.env 权限', en: '.env permission', icon: '📂' },
@@ -161,6 +161,18 @@ export function renderHtmlReport(report, scan, locale, meta) {
161
161
  }
162
162
  S.push('</tbody></table></div>');
163
163
  }
164
+ // ===== 接入运行时指引:把 ⚪ 待核验项变成可验证(回答"需要接入才能测")=====
165
+ if (report.staticScan && report.manual > 0) {
166
+ const cfg = `{
167
+ "mcpServers": {
168
+ "shellward": { "command": "npx", "args": ["-y", "-p", "shellward", "shellward-mcp"] }
169
+ }
170
+ }`;
171
+ S.push(sectionHead('🔌', t('让待核验项可验证:接入运行时', 'Make ⚪ items verifiable: deploy runtime'), t('静态扫描测不了运行时行为,接入后这些项才能被持续验证', 'Static scan cannot test runtime behavior; deploy to validate')));
172
+ S.push(`<p class="note">${t(`把下面这段加到 Claude Desktop / Cursor / OpenClaw 的 MCP 配置,重启后 ShellWard 就作为<b>运行时防护</b>在你的 AI 应用里运行——上方 ${report.manual} 项(审计留存、运行时拦截、数据外发管控等)即可被真实验证。`, `Add this to your Claude Desktop / Cursor / OpenClaw MCP config and restart — ShellWard then runs as a <b>runtime guard</b>, validating the ${report.manual} ⚪ items above.`)}</p>`);
173
+ S.push(`<div class="cfg"><pre id="mcpcfg">${esc(cfg)}</pre><button onclick="(function(b){navigator.clipboard&&navigator.clipboard.writeText(document.getElementById('mcpcfg').textContent).then(function(){b.textContent='${t('已复制', 'Copied')}'})})(this)">${t('复制', 'Copy')}</button></div>`);
174
+ S.push(`<p class="muted">${t('或命令行直接起:', 'Or run directly:')} <code>npx shellward mcp</code> · ${t('文档', 'Docs')}: <a href="https://github.com/jnMetaCode/shellward">github.com/jnMetaCode/shellward</a></p>`);
175
+ }
164
176
  const disclaimer = t('本报告由 ShellWard 合规网关自动生成,帮助评估并满足合规技术要求,不构成法律意见,亦不替代算法备案/定级备案/PIA 等主体责任。⚪ 待确认项需结合业务人工判定。', 'Generated by ShellWard Compliance Gateway. Assists with technical compliance; not legal advice. ⚪ items require manual review.');
165
177
  return `<!DOCTYPE html>
166
178
  <html lang="${zh ? 'zh-CN' : 'en'}">
@@ -323,6 +335,12 @@ table.tbl td.right{width:64px}
323
335
  .manual-note{margin:8px 36px 12px;font-size:13px;color:#475569;background:#eff6ff;
324
336
  border-left:3px solid #3b82f6;line-height:1.6}
325
337
  .manual-note code{background:#dbeafe;padding:1px 6px;border-radius:5px}
338
+ .cfg{position:relative;margin:8px 36px}
339
+ .cfg pre{background:#0f172a;color:#e2e8f0;border-radius:10px;padding:16px 18px;margin:0;
340
+ font-family:ui-monospace,Menlo,monospace;font-size:12.5px;overflow-x:auto;line-height:1.5}
341
+ .cfg button{position:absolute;top:10px;right:12px;background:#1e293b;color:#94a3b8;border:0;
342
+ border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer}
343
+ .cfg button:hover{color:#fff}
326
344
 
327
345
  /* 法规分组 */
328
346
  .reg{margin:14px 36px;padding:0;border:1px solid var(--line);border-radius:12px;overflow:hidden}
Binary file
@@ -116,7 +116,7 @@ export function renderComplianceReport(report, locale) {
116
116
  return L.join('\n');
117
117
  }
118
118
  const KIND_LABEL = {
119
- overseas: { zh: '数据出境风险', en: 'Data export risk', icon: '🌐' },
119
+ overseas: { zh: '境外大模型调用(需评估出境)', en: 'Overseas LLM (assess export)', icon: '🌐' },
120
120
  secret: { zh: '硬编码密钥', en: 'Hardcoded secret', icon: '🔑' },
121
121
  pii: { zh: '个人信息暴露', en: 'PII exposure', icon: '🪪' },
122
122
  'env-perm': { zh: '.env 权限', en: '.env permission', icon: '📂' },
package/dist/init.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /** 标准 MCP 接入条目:零安装,npx 拉取已发布的 shellward-mcp */
2
+ export declare const SHELLWARD_MCP_ENTRY: {
3
+ command: string;
4
+ args: string[];
5
+ };
6
+ export interface InitTarget {
7
+ name: string;
8
+ path: string;
9
+ /** 配置里放 MCP 服务器的字段名(绝大多数是 mcpServers) */
10
+ key: string;
11
+ /** 工具未安装时是否允许新建配置文件 */
12
+ createIfMissing?: boolean;
13
+ }
14
+ /** 已知 AI 工具的 MCP 配置位置(跨平台) */
15
+ export declare function knownTargets(home?: string): InitTarget[];
16
+ export type MergeResult = {
17
+ status: 'added' | 'updated';
18
+ config: any;
19
+ } | {
20
+ status: 'unchanged';
21
+ config: any;
22
+ };
23
+ /**
24
+ * 纯合并:把 shellward 条目并入配置对象。已存在且相同→unchanged;不同→updated;没有→added。
25
+ * 不破坏其它 MCP 服务器条目。
26
+ */
27
+ export declare function mergeShellward(config: any, key: string): MergeResult;
28
+ export interface InitOutcome {
29
+ name: string;
30
+ path: string;
31
+ result: 'added' | 'updated' | 'unchanged' | 'skipped' | 'error';
32
+ detail?: string;
33
+ }
34
+ /** 执行接入:探测→读取→合并→备份→写回。dryRun 仅预览不写。 */
35
+ export declare function runInit(opts?: {
36
+ dryRun?: boolean;
37
+ home?: string;
38
+ }): InitOutcome[];
package/dist/init.js ADDED
@@ -0,0 +1,77 @@
1
+ // src/init.ts — `shellward init`:一条命令把 ShellWard 接入已安装的 AI 工具(MCP 运行时防护)
2
+ //
3
+ // 把"扫描 → 运行时防护"的部署摩擦降到一条命令:自动探测 Claude Desktop / Cursor /
4
+ // Claude Code / Windsurf 的 MCP 配置,安全地加入 shellward 条目(备份、合并、不覆盖)。
5
+ // 这是「安装按钮」的正确形态——只对已知配置文件操作、改前备份、可 --dry-run 预览。
6
+ import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'fs';
7
+ import { join, dirname } from 'path';
8
+ import { homedir } from 'os';
9
+ /** 标准 MCP 接入条目:零安装,npx 拉取已发布的 shellward-mcp */
10
+ export const SHELLWARD_MCP_ENTRY = {
11
+ command: 'npx',
12
+ args: ['-y', '-p', 'shellward', 'shellward-mcp'],
13
+ };
14
+ /** 已知 AI 工具的 MCP 配置位置(跨平台) */
15
+ export function knownTargets(home = homedir()) {
16
+ const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
17
+ const claudeDesktop = process.platform === 'darwin' ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
18
+ : process.platform === 'win32' ? join(appData, 'Claude', 'claude_desktop_config.json')
19
+ : join(home, '.config', 'Claude', 'claude_desktop_config.json');
20
+ return [
21
+ { name: 'Claude Desktop', path: claudeDesktop, key: 'mcpServers', createIfMissing: true },
22
+ { name: 'Cursor', path: join(home, '.cursor', 'mcp.json'), key: 'mcpServers', createIfMissing: true },
23
+ { name: 'Claude Code', path: join(home, '.claude.json'), key: 'mcpServers' },
24
+ { name: 'Windsurf', path: join(home, '.codeium', 'windsurf', 'mcp_config.json'), key: 'mcpServers' },
25
+ ];
26
+ }
27
+ /**
28
+ * 纯合并:把 shellward 条目并入配置对象。已存在且相同→unchanged;不同→updated;没有→added。
29
+ * 不破坏其它 MCP 服务器条目。
30
+ */
31
+ export function mergeShellward(config, key) {
32
+ const cfg = config && typeof config === 'object' ? config : {};
33
+ const servers = cfg[key] && typeof cfg[key] === 'object' ? cfg[key] : {};
34
+ const existing = servers.shellward;
35
+ const same = existing && JSON.stringify(existing) === JSON.stringify(SHELLWARD_MCP_ENTRY);
36
+ if (same)
37
+ return { status: 'unchanged', config: cfg };
38
+ const status = existing ? 'updated' : 'added';
39
+ cfg[key] = { ...servers, shellward: { ...SHELLWARD_MCP_ENTRY } };
40
+ return { status, config: cfg };
41
+ }
42
+ /** 执行接入:探测→读取→合并→备份→写回。dryRun 仅预览不写。 */
43
+ export function runInit(opts = {}) {
44
+ const targets = knownTargets(opts.home);
45
+ const out = [];
46
+ for (const t of targets) {
47
+ const exists = existsSync(t.path);
48
+ if (!exists && !t.createIfMissing) {
49
+ out.push({ name: t.name, path: t.path, result: 'skipped', detail: '未安装/无配置' });
50
+ continue;
51
+ }
52
+ try {
53
+ let config = {};
54
+ if (exists) {
55
+ const raw = readFileSync(t.path, 'utf-8').trim();
56
+ config = raw ? JSON.parse(raw) : {};
57
+ }
58
+ const merged = mergeShellward(config, t.key);
59
+ if (merged.status === 'unchanged') {
60
+ out.push({ name: t.name, path: t.path, result: 'unchanged', detail: '已接入' });
61
+ continue;
62
+ }
63
+ if (!opts.dryRun) {
64
+ if (exists)
65
+ copyFileSync(t.path, t.path + '.shellward.bak'); // 改前备份
66
+ else
67
+ mkdirSync(dirname(t.path), { recursive: true });
68
+ writeFileSync(t.path, JSON.stringify(merged.config, null, 2) + '\n');
69
+ }
70
+ out.push({ name: t.name, path: t.path, result: merged.status, detail: opts.dryRun ? '预览(未写入)' : (exists ? '已加入(原文件已备份 .bak)' : '已新建配置') });
71
+ }
72
+ catch (e) {
73
+ out.push({ name: t.name, path: t.path, result: 'error', detail: e?.message || String(e) });
74
+ }
75
+ }
76
+ return out;
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.7.12",
3
+ "version": "0.7.14",
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,7 +57,7 @@
57
57
  "scripts": {
58
58
  "build": "tsc",
59
59
  "mcp": "npx tsx src/mcp-server.ts",
60
- "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-rugpull.ts && npx tsx test-redos.ts && npx tsx test-mcp-client.ts && npx tsx test-mcp.ts && npx tsx test-compliance.ts && npx tsx test-web.ts",
60
+ "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-rugpull.ts && npx tsx test-redos.ts && npx tsx test-mcp-client.ts && npx tsx test-mcp.ts && npx tsx test-compliance.ts && npx tsx test-web.ts && npx tsx test-init.ts",
61
61
  "test:redos": "npx tsx test-redos.ts",
62
62
  "test:compliance": "npx tsx test-compliance.ts",
63
63
  "test:integration": "npx tsx test-integration.ts",
@@ -66,7 +66,8 @@
66
66
  "test:mcp": "npx tsx test-mcp.ts",
67
67
  "bench": "npx tsx bench/run.ts",
68
68
  "bench:scan": "npx tsx bench/scan-bench.ts",
69
- "prepublishOnly": "npm run build"
69
+ "prepublishOnly": "npm run build",
70
+ "test:init": "npx tsx test-init.ts"
70
71
  },
71
72
  "openclaw": {
72
73
  "extensions": [
package/src/cli.ts CHANGED
@@ -18,6 +18,7 @@ import { ShellWard } from './core/engine.js'
18
18
  import { runProjectComplianceAudit } from './compliance/audit.js'
19
19
  import { renderComplianceReport, renderProjectFindings } from './compliance/report.js'
20
20
  import { renderHtmlReport } from './compliance/html-report.js'
21
+ import { runInit } from './init.js'
21
22
  import { resolveLocale } from './types.js'
22
23
 
23
24
  const argv = process.argv.slice(2)
@@ -36,6 +37,11 @@ async function main() {
36
37
  return
37
38
  }
38
39
 
40
+ if (cmd === 'init') {
41
+ runInitCommand(argv.includes('--dry-run'))
42
+ return
43
+ }
44
+
39
45
  if (cmd === 'web') {
40
46
  const { startWebServer } = await import('./web/scan-server.js')
41
47
  const local = argv.includes('--local')
@@ -163,6 +169,30 @@ function runScan(args: string[]) {
163
169
  }
164
170
  }
165
171
 
172
+ /** `shellward init`:把 ShellWard 接入已安装 AI 工具的 MCP 配置(运行时防护) */
173
+ function runInitCommand(dryRun: boolean): void {
174
+ const out = runInit({ dryRun })
175
+ console.log('\n🔌 ShellWard 接入 AI 工具运行时防护' + (dryRun ? '(预览,不写入)' : '') + '\n')
176
+ const ICON: Record<string, string> = { added: '✅', updated: '✅', unchanged: '✔️', skipped: '·', error: '⚠️' }
177
+ let touched = 0
178
+ for (const o of out) {
179
+ const label = { added: '已接入', updated: '已更新', unchanged: '已接入(无变化)', skipped: '跳过', error: '失败' }[o.result]
180
+ if (o.result === 'added' || o.result === 'updated') touched++
181
+ console.log(` ${ICON[o.result] || '·'} ${o.name} — ${label}${o.detail ? ':' + o.detail : ''}`)
182
+ if (o.result !== 'skipped') console.log(` ${o.path}`)
183
+ }
184
+ console.log('')
185
+ if (dryRun) {
186
+ console.log('这是预览。去掉 --dry-run 实际接入。')
187
+ } else if (touched > 0) {
188
+ console.log('✅ 已接入。请重启对应的 AI 工具,ShellWard 即作为运行时防护生效(拦注入/外泄/危险命令)。')
189
+ console.log(' 验证:在工具里问"调用 shellward 的 security_status"。原配置已备份为 *.shellward.bak。')
190
+ } else {
191
+ console.log('未发现可接入的已安装 AI 工具配置。也可手动加 MCP:')
192
+ console.log(' {"mcpServers":{"shellward":{"command":"npx","args":["-y","-p","shellward","shellward-mcp"]}}}')
193
+ }
194
+ }
195
+
166
196
  /** 跨平台在默认浏览器打开 URL 或本地文件(失败静默,不影响主流程) */
167
197
  function openBrowser(target: string): void {
168
198
  const cmd = process.platform === 'darwin' ? 'open'
@@ -199,6 +229,7 @@ Usage:
199
229
  shellward scan --serve Scan and serve the report at http://localhost (local)
200
230
  shellward web [port] Web scanner for public repo URLs (deploy this)
201
231
  shellward web --local Local web GUI: scan a local path (private, no upload)
232
+ shellward init Install ShellWard into your AI tools (MCP runtime guard)
202
233
  shellward mcp Start MCP server (stdio)
203
234
  shellward --help
204
235
 
@@ -217,6 +248,7 @@ PII in files, .env permissions. Maps to CSL / PIPL / MLPS / cross-border / label
217
248
  shellward scan --serve 扫描并在 http://localhost 提供报告(本地服务)
218
249
  shellward web [端口] 公开仓库 web 扫描器(贴 URL 体检,用于部署)
219
250
  shellward web --local 本地 web GUI:填本地路径扫描(私有、不上传,客户端体验)
251
+ shellward init 一键接入你的 AI 工具(MCP 运行时防护,--dry-run 预览)
220
252
  shellward mcp 启动 MCP 服务器(stdio)
221
253
  shellward --help
222
254
 
@@ -317,11 +317,12 @@ function checkEnv(c: ComplianceControl, env: EnvFacts): ControlResult {
317
317
  if (env.overseas.length > 0) {
318
318
  const names = env.overseas.map(o => o.provider_zh).join(', ')
319
319
  const namesEn = env.overseas.map(o => o.provider_en).join(', ')
320
- return mk(c, 'fail',
321
- `检测到境外大模型端点配置: ${names} — 若向其发送个人信息/重要数据即构成数据出境,须走合规路径`,
322
- `Overseas LLM endpoint(s) configured: ${namesEn} — sending PI/important data = cross-border export`)
320
+ // 检出境外调用是"事实",是否违规取决于是否发送 PII/重要数据 → 标"需评估"而非"不合规"
321
+ return mk(c, 'warn',
322
+ `检测到境外大模型调用: ${names}。是否违规取决于发送的数据:涉及个人信息/重要数据需走合规路径或改用境内模型;仅非个人/非重要数据通常可接受。`,
323
+ `Overseas LLM detected: ${namesEn}. Compliance depends on the data sent — PI/important data needs a compliant path; non-personal data is usually fine.`)
323
324
  }
324
- return mk(c, 'pass', '未检测到境外大模型端点配置', 'No overseas LLM endpoint detected')
325
+ return mk(c, 'pass', '未检测到境外大模型调用', 'No overseas LLM detected')
325
326
  }
326
327
  return mk(c, 'manual', '需人工确认', 'Manual check required')
327
328
  }
@@ -20,7 +20,7 @@ const STATUS: Record<ControlStatus, { zh: string; en: string; cls: string }> = {
20
20
  }
21
21
 
22
22
  const KIND: Record<FindingKind, { zh: string; en: string; icon: string }> = {
23
- overseas: { zh: '数据出境风险', en: 'Data export risk', icon: '🌐' },
23
+ overseas: { zh: '境外大模型调用(需评估出境)', en: 'Overseas LLM (assess export)', icon: '🌐' },
24
24
  secret: { zh: '硬编码密钥', en: 'Hardcoded secret', icon: '🔑' },
25
25
  pii: { zh: '个人信息暴露', en: 'PII exposure', icon: '🪪' },
26
26
  'env-perm': { zh: '.env 权限', en: '.env permission', icon: '📂' },
@@ -192,6 +192,22 @@ export function renderHtmlReport(
192
192
  S.push('</tbody></table></div>')
193
193
  }
194
194
 
195
+ // ===== 接入运行时指引:把 ⚪ 待核验项变成可验证(回答"需要接入才能测")=====
196
+ if (report.staticScan && report.manual > 0) {
197
+ const cfg = `{
198
+ "mcpServers": {
199
+ "shellward": { "command": "npx", "args": ["-y", "-p", "shellward", "shellward-mcp"] }
200
+ }
201
+ }`
202
+ S.push(sectionHead('🔌', t('让待核验项可验证:接入运行时', 'Make ⚪ items verifiable: deploy runtime'),
203
+ t('静态扫描测不了运行时行为,接入后这些项才能被持续验证', 'Static scan cannot test runtime behavior; deploy to validate')))
204
+ S.push(`<p class="note">${t(
205
+ `把下面这段加到 Claude Desktop / Cursor / OpenClaw 的 MCP 配置,重启后 ShellWard 就作为<b>运行时防护</b>在你的 AI 应用里运行——上方 ${report.manual} 项(审计留存、运行时拦截、数据外发管控等)即可被真实验证。`,
206
+ `Add this to your Claude Desktop / Cursor / OpenClaw MCP config and restart — ShellWard then runs as a <b>runtime guard</b>, validating the ${report.manual} ⚪ items above.`)}</p>`)
207
+ S.push(`<div class="cfg"><pre id="mcpcfg">${esc(cfg)}</pre><button onclick="(function(b){navigator.clipboard&&navigator.clipboard.writeText(document.getElementById('mcpcfg').textContent).then(function(){b.textContent='${t('已复制', 'Copied')}'})})(this)">${t('复制', 'Copy')}</button></div>`)
208
+ S.push(`<p class="muted">${t('或命令行直接起:', 'Or run directly:')} <code>npx shellward mcp</code> · ${t('文档', 'Docs')}: <a href="https://github.com/jnMetaCode/shellward">github.com/jnMetaCode/shellward</a></p>`)
209
+ }
210
+
195
211
  const disclaimer = t(
196
212
  '本报告由 ShellWard 合规网关自动生成,帮助评估并满足合规技术要求,不构成法律意见,亦不替代算法备案/定级备案/PIA 等主体责任。⚪ 待确认项需结合业务人工判定。',
197
213
  'Generated by ShellWard Compliance Gateway. Assists with technical compliance; not legal advice. ⚪ items require manual review.')
@@ -364,6 +380,12 @@ table.tbl td.right{width:64px}
364
380
  .manual-note{margin:8px 36px 12px;font-size:13px;color:#475569;background:#eff6ff;
365
381
  border-left:3px solid #3b82f6;line-height:1.6}
366
382
  .manual-note code{background:#dbeafe;padding:1px 6px;border-radius:5px}
383
+ .cfg{position:relative;margin:8px 36px}
384
+ .cfg pre{background:#0f172a;color:#e2e8f0;border-radius:10px;padding:16px 18px;margin:0;
385
+ font-family:ui-monospace,Menlo,monospace;font-size:12.5px;overflow-x:auto;line-height:1.5}
386
+ .cfg button{position:absolute;top:10px;right:12px;background:#1e293b;color:#94a3b8;border:0;
387
+ border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer}
388
+ .cfg button:hover{color:#fff}
367
389
 
368
390
  /* 法规分组 */
369
391
  .reg{margin:14px 36px;padding:0;border:1px solid var(--line);border-radius:12px;overflow:hidden}
Binary file
@@ -130,7 +130,7 @@ export function renderComplianceReport(report: ComplianceReport, locale: 'zh' |
130
130
  }
131
131
 
132
132
  const KIND_LABEL: Record<FindingKind, { zh: string; en: string; icon: string }> = {
133
- overseas: { zh: '数据出境风险', en: 'Data export risk', icon: '🌐' },
133
+ overseas: { zh: '境外大模型调用(需评估出境)', en: 'Overseas LLM (assess export)', icon: '🌐' },
134
134
  secret: { zh: '硬编码密钥', en: 'Hardcoded secret', icon: '🔑' },
135
135
  pii: { zh: '个人信息暴露', en: 'PII exposure', icon: '🪪' },
136
136
  'env-perm': { zh: '.env 权限', en: '.env permission', icon: '📂' },
package/src/init.ts ADDED
@@ -0,0 +1,99 @@
1
+ // src/init.ts — `shellward init`:一条命令把 ShellWard 接入已安装的 AI 工具(MCP 运行时防护)
2
+ //
3
+ // 把"扫描 → 运行时防护"的部署摩擦降到一条命令:自动探测 Claude Desktop / Cursor /
4
+ // Claude Code / Windsurf 的 MCP 配置,安全地加入 shellward 条目(备份、合并、不覆盖)。
5
+ // 这是「安装按钮」的正确形态——只对已知配置文件操作、改前备份、可 --dry-run 预览。
6
+
7
+ import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'fs'
8
+ import { join, dirname } from 'path'
9
+ import { homedir } from 'os'
10
+
11
+ /** 标准 MCP 接入条目:零安装,npx 拉取已发布的 shellward-mcp */
12
+ export const SHELLWARD_MCP_ENTRY = {
13
+ command: 'npx',
14
+ args: ['-y', '-p', 'shellward', 'shellward-mcp'],
15
+ }
16
+
17
+ export interface InitTarget {
18
+ name: string
19
+ path: string
20
+ /** 配置里放 MCP 服务器的字段名(绝大多数是 mcpServers) */
21
+ key: string
22
+ /** 工具未安装时是否允许新建配置文件 */
23
+ createIfMissing?: boolean
24
+ }
25
+
26
+ /** 已知 AI 工具的 MCP 配置位置(跨平台) */
27
+ export function knownTargets(home = homedir()): InitTarget[] {
28
+ const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming')
29
+ const claudeDesktop =
30
+ process.platform === 'darwin' ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
31
+ : process.platform === 'win32' ? join(appData, 'Claude', 'claude_desktop_config.json')
32
+ : join(home, '.config', 'Claude', 'claude_desktop_config.json')
33
+ return [
34
+ { name: 'Claude Desktop', path: claudeDesktop, key: 'mcpServers', createIfMissing: true },
35
+ { name: 'Cursor', path: join(home, '.cursor', 'mcp.json'), key: 'mcpServers', createIfMissing: true },
36
+ { name: 'Claude Code', path: join(home, '.claude.json'), key: 'mcpServers' },
37
+ { name: 'Windsurf', path: join(home, '.codeium', 'windsurf', 'mcp_config.json'), key: 'mcpServers' },
38
+ ]
39
+ }
40
+
41
+ export type MergeResult =
42
+ | { status: 'added' | 'updated'; config: any }
43
+ | { status: 'unchanged'; config: any }
44
+
45
+ /**
46
+ * 纯合并:把 shellward 条目并入配置对象。已存在且相同→unchanged;不同→updated;没有→added。
47
+ * 不破坏其它 MCP 服务器条目。
48
+ */
49
+ export function mergeShellward(config: any, key: string): MergeResult {
50
+ const cfg = config && typeof config === 'object' ? config : {}
51
+ const servers = cfg[key] && typeof cfg[key] === 'object' ? cfg[key] : {}
52
+ const existing = servers.shellward
53
+ const same = existing && JSON.stringify(existing) === JSON.stringify(SHELLWARD_MCP_ENTRY)
54
+ if (same) return { status: 'unchanged', config: cfg }
55
+ const status = existing ? 'updated' : 'added'
56
+ cfg[key] = { ...servers, shellward: { ...SHELLWARD_MCP_ENTRY } }
57
+ return { status, config: cfg }
58
+ }
59
+
60
+ export interface InitOutcome {
61
+ name: string
62
+ path: string
63
+ result: 'added' | 'updated' | 'unchanged' | 'skipped' | 'error'
64
+ detail?: string
65
+ }
66
+
67
+ /** 执行接入:探测→读取→合并→备份→写回。dryRun 仅预览不写。 */
68
+ export function runInit(opts: { dryRun?: boolean; home?: string } = {}): InitOutcome[] {
69
+ const targets = knownTargets(opts.home)
70
+ const out: InitOutcome[] = []
71
+ for (const t of targets) {
72
+ const exists = existsSync(t.path)
73
+ if (!exists && !t.createIfMissing) {
74
+ out.push({ name: t.name, path: t.path, result: 'skipped', detail: '未安装/无配置' })
75
+ continue
76
+ }
77
+ try {
78
+ let config: any = {}
79
+ if (exists) {
80
+ const raw = readFileSync(t.path, 'utf-8').trim()
81
+ config = raw ? JSON.parse(raw) : {}
82
+ }
83
+ const merged = mergeShellward(config, t.key)
84
+ if (merged.status === 'unchanged') {
85
+ out.push({ name: t.name, path: t.path, result: 'unchanged', detail: '已接入' })
86
+ continue
87
+ }
88
+ if (!opts.dryRun) {
89
+ if (exists) copyFileSync(t.path, t.path + '.shellward.bak') // 改前备份
90
+ else mkdirSync(dirname(t.path), { recursive: true })
91
+ writeFileSync(t.path, JSON.stringify(merged.config, null, 2) + '\n')
92
+ }
93
+ out.push({ name: t.name, path: t.path, result: merged.status, detail: opts.dryRun ? '预览(未写入)' : (exists ? '已加入(原文件已备份 .bak)' : '已新建配置') })
94
+ } catch (e: any) {
95
+ out.push({ name: t.name, path: t.path, result: 'error', detail: e?.message || String(e) })
96
+ }
97
+ }
98
+ return out
99
+ }