shellward 0.6.1 → 0.6.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.
@@ -0,0 +1,215 @@
1
+ // src/compliance/report.ts — 合规体检报告 Markdown 渲染
2
+ //
3
+ // 把 ComplianceReport 渲染成红黄绿评分卡(可截图传播 = 月1 获客钩子)。
4
+ // 按法规分组,每项给状态图标 + 结论 + 整改建议。
5
+ import { REGULATION_NAMES } from './regulations.js';
6
+ import { suggestDomestic } from '../rules/domestic-alternatives.js';
7
+ const STATUS_ICON = {
8
+ pass: '🟢',
9
+ warn: '🟡',
10
+ fail: '🔴',
11
+ manual: '⚪',
12
+ };
13
+ const STATUS_TEXT_ZH = {
14
+ pass: '合规', warn: '部分', fail: '不合规', manual: '待确认',
15
+ };
16
+ const STATUS_TEXT_EN = {
17
+ pass: 'Pass', warn: 'Partial', fail: 'Fail', manual: 'Manual',
18
+ };
19
+ const REGULATION_ORDER = ['CSL', 'PIPL', 'MLPS', 'CBDT', 'GENAI'];
20
+ /** 渲染合规体检报告为 Markdown。locale 决定中英文。 */
21
+ export function renderComplianceReport(report, locale) {
22
+ const zh = locale === 'zh';
23
+ const L = [];
24
+ // ===== 标题 + 总评分卡 =====
25
+ L.push(zh ? '# 🛡️ AI 应用合规体检报告' : '# 🛡️ AI Application Compliance Report');
26
+ L.push('');
27
+ L.push(zh
28
+ ? `> ShellWard 合规网关自动生成 · ${report.generatedAt.slice(0, 19).replace('T', ' ')} UTC`
29
+ : `> Generated by ShellWard Compliance Gateway · ${report.generatedAt.slice(0, 19).replace('T', ' ')} UTC`);
30
+ L.push('');
31
+ const bar = scoreBar(report.score);
32
+ L.push(zh ? '## 总评' : '## Overall');
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('```');
39
+ L.push('');
40
+ 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)`);
43
+ if (report.projectPenalty && report.projectPenalty > 0) {
44
+ L.push('');
45
+ L.push(zh
46
+ ? `> 含项目实测风险扣分 **-${report.projectPenalty}**(见下方「项目实测风险」)`
47
+ : `> Includes **-${report.projectPenalty}** from project scan findings (see Project Scan Findings)`);
48
+ }
49
+ L.push('');
50
+ // ===== 优先整改(fail 项,按严重度) =====
51
+ const fails = report.results
52
+ .filter(r => r.status === 'fail')
53
+ .sort((a, b) => severityRank(b) - severityRank(a));
54
+ if (fails.length > 0) {
55
+ L.push(zh ? '## ⚠️ 优先整改项' : '## ⚠️ Priority Fixes');
56
+ L.push('');
57
+ for (const r of fails) {
58
+ L.push(`- 🔴 **${zh ? r.control.title_zh : r.control.title_en}** `
59
+ + `(${regName(r.control.regulation, zh)} ${r.control.article})`);
60
+ L.push(` - ${zh ? '整改' : 'Fix'}: ${zh ? r.control.remediation_zh : r.control.remediation_en}`);
61
+ }
62
+ L.push('');
63
+ }
64
+ // ===== 按法规分组明细 =====
65
+ L.push(zh ? '## 分项明细' : '## Detailed Results');
66
+ L.push('');
67
+ const grouped = groupByRegulation(report.results);
68
+ for (const reg of REGULATION_ORDER) {
69
+ const items = grouped[reg];
70
+ if (!items || items.length === 0)
71
+ continue;
72
+ L.push(`### ${regName(reg, zh)}`);
73
+ L.push('');
74
+ L.push(zh
75
+ ? '| 状态 | 控制项 | 条款 | 结论 |'
76
+ : '| Status | Control | Article | Result |');
77
+ L.push('|---|---|---|---|');
78
+ for (const r of items) {
79
+ const icon = STATUS_ICON[r.status];
80
+ const txt = zh ? STATUS_TEXT_ZH[r.status] : STATUS_TEXT_EN[r.status];
81
+ const title = zh ? r.control.title_zh : r.control.title_en;
82
+ const detail = (zh ? r.detail_zh : r.detail_en).replace(/\|/g, '/').replace(/\n/g, ' ');
83
+ L.push(`| ${icon} ${txt} | ${title} | ${r.control.article} | ${detail} |`);
84
+ }
85
+ L.push('');
86
+ }
87
+ // ===== 免责声明 =====
88
+ L.push('---');
89
+ L.push(zh
90
+ ? '> **说明**:本报告帮助评估并满足合规技术要求,不构成法律意见,亦不替代算法备案/定级备案/PIA 等主体责任。⚪ 待确认项需结合业务人工判定。'
91
+ : '> **Note**: This report assists with technical compliance and is not legal advice. ⚪ items require manual review.');
92
+ return L.join('\n');
93
+ }
94
+ const KIND_LABEL = {
95
+ overseas: { zh: '数据出境风险', en: 'Data export risk', icon: '🌐' },
96
+ secret: { zh: '硬编码密钥', en: 'Hardcoded secret', icon: '🔑' },
97
+ pii: { zh: '个人信息暴露', en: 'PII exposure', icon: '🪪' },
98
+ 'env-perm': { zh: '.env 权限', en: '.env permission', icon: '📂' },
99
+ };
100
+ const KIND_ORDER = ['overseas', 'secret', 'pii', 'env-perm'];
101
+ /**
102
+ * 渲染「项目实测风险」段 —— 关于用户项目的真实发现 (文件:行),
103
+ * 这是评分卡可截图、可信的关键。附加在合规报告之后。
104
+ */
105
+ export function renderProjectFindings(scan, locale) {
106
+ const zh = locale === 'zh';
107
+ const L = [];
108
+ L.push(zh ? '## 🔍 项目实测风险' : '## 🔍 Project Scan Findings');
109
+ L.push('');
110
+ L.push(zh
111
+ ? `> 已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限,部分未扫)' : ''}`
112
+ : `> Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached, partial)' : ''}`);
113
+ L.push('');
114
+ if (scan.findings.length === 0) {
115
+ L.push(zh ? '🟢 未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。' : '🟢 No hardcoded secrets, PII exposure, or overseas endpoints found in project files.');
116
+ L.push('');
117
+ return L.join('\n');
118
+ }
119
+ // 概览计数
120
+ const counts = KIND_ORDER
121
+ .filter(k => scan.counts[k] > 0)
122
+ .map(k => `${KIND_LABEL[k].icon} ${zh ? KIND_LABEL[k].zh : KIND_LABEL[k].en}: ${scan.counts[k]}`)
123
+ .join(' | ');
124
+ L.push(`**${counts}**`);
125
+ L.push('');
126
+ // 按类型分组列出(每类最多 15 条,避免报告过长)
127
+ for (const kind of KIND_ORDER) {
128
+ const items = scan.findings.filter(f => f.kind === kind);
129
+ if (items.length === 0)
130
+ continue;
131
+ const label = KIND_LABEL[kind];
132
+ L.push(`### ${label.icon} ${zh ? label.zh : label.en} (${items.length})`);
133
+ L.push('');
134
+ for (const f of items.slice(0, 15)) {
135
+ const loc = f.line ? `${f.file}:${f.line}` : f.file;
136
+ L.push(`- \`${loc}\` — ${f.detail}`);
137
+ }
138
+ if (items.length > 15) {
139
+ L.push(zh ? `- …其余 ${items.length - 15} 项` : `- …and ${items.length - 15} more`);
140
+ }
141
+ L.push('');
142
+ }
143
+ // 处方:境内合规替代建议(仅当存在境外模型风险时)
144
+ L.push(...renderDomesticGuidance(scan, locale));
145
+ return L.join('\n');
146
+ }
147
+ /** 渲染「境内合规替代建议」—— 把数据出境风险变成可执行的迁移处方 */
148
+ function renderDomesticGuidance(scan, locale) {
149
+ const zh = locale === 'zh';
150
+ const overseas = scan.findings.filter(f => f.kind === 'overseas');
151
+ if (overseas.length === 0)
152
+ return [];
153
+ // 去重境外厂商
154
+ const seen = new Set();
155
+ const providers = [];
156
+ for (const f of overseas) {
157
+ const key = (f.endpointId || f.provider_en || f.provider_zh || '').toLowerCase();
158
+ if (!key || seen.has(key))
159
+ continue;
160
+ seen.add(key);
161
+ providers.push({ key: f.endpointId || f.provider_en || '', zh: f.provider_zh, en: f.provider_en });
162
+ }
163
+ const L = [];
164
+ L.push(zh ? '## ✅ 境内合规替代建议' : '## ✅ Domestic Compliance Alternatives');
165
+ L.push('');
166
+ L.push(zh
167
+ ? '把数据出境风险变成可执行的迁移路径。境内主流模型多为 **OpenAI 兼容**接口:'
168
+ : 'Turn data-export risk into a concrete migration path. Most domestic models are **OpenAI-compatible**:');
169
+ L.push('');
170
+ // 每个境外厂商的迁移难度
171
+ for (const p of providers) {
172
+ const s = suggestDomestic(p.key, p.zh, p.en);
173
+ L.push(zh
174
+ ? `- **${s.overseas_zh}** → 迁移难度: ${s.difficulty_zh}`
175
+ : `- **${s.overseas_en}** → migration: ${s.difficulty_en}`);
176
+ }
177
+ L.push('');
178
+ // 共享的境内候选表(取第一个建议的 alternatives,避免重复)
179
+ const alts = suggestDomestic(providers[0].key, providers[0].zh, providers[0].en).alternatives;
180
+ L.push(zh ? '| 境内模型 | 厂商 | OpenAI 兼容 base_url |' : '| Domestic model | Vendor | OpenAI-compatible base_url |');
181
+ L.push('|---|---|---|');
182
+ for (const m of alts) {
183
+ L.push(`| ${zh ? m.name_zh : m.name_en} | ${m.vendor_zh} | \`${m.baseUrl}\` |`);
184
+ }
185
+ L.push('');
186
+ L.push(zh
187
+ ? '> 对使用 `openai` SDK 的项目:通常仅需把 `base_url` 与 `api_key` 换成上表任一境内模型即可,业务代码无需改动。迁移前请以各厂商官方文档为准。'
188
+ : '> For projects using the `openai` SDK: usually just swap `base_url` + `api_key` to a domestic model above — no business-code change. Verify against each vendor’s official docs first.');
189
+ L.push('');
190
+ return L;
191
+ }
192
+ function scoreBar(score) {
193
+ const filled = Math.round(score / 5);
194
+ return '█'.repeat(filled) + '░'.repeat(20 - filled);
195
+ }
196
+ function gradeBadge(grade) {
197
+ const map = {
198
+ A: '🏆 优秀', B: '✅ 良好', C: '⚠️ 及格', D: '❌ 不及格',
199
+ };
200
+ return map[grade] || grade;
201
+ }
202
+ function severityRank(r) {
203
+ return { critical: 4, high: 3, medium: 2, low: 1 }[r.control.severity];
204
+ }
205
+ function regName(reg, zh) {
206
+ return zh ? REGULATION_NAMES[reg].zh : REGULATION_NAMES[reg].en;
207
+ }
208
+ function groupByRegulation(results) {
209
+ const out = {};
210
+ for (const r of results) {
211
+ ;
212
+ (out[r.control.regulation] ||= []).push(r);
213
+ }
214
+ return out;
215
+ }
@@ -21,6 +21,8 @@
21
21
  // }
22
22
  import { ShellWard } from './core/engine.js';
23
23
  import { McpBaseline } from './mcp-baseline.js';
24
+ import { runComplianceAudit } from './compliance/audit.js';
25
+ import { renderComplianceReport } from './compliance/report.js';
24
26
  import { readFileSync } from 'fs';
25
27
  import { createInterface } from 'readline';
26
28
  import { fileURLToPath } from 'url';
@@ -140,6 +142,16 @@ const TOOLS = [
140
142
  properties: {},
141
143
  },
142
144
  },
145
+ {
146
+ name: 'compliance_check',
147
+ description: 'Run a China AI-compliance health check (网安法/PIPL/等保2.0/数据出境/AI标识) and return a red/yellow/green scorecard report. Detects overseas LLM endpoints (data-export risk), audit-log retention, enabled defense layers, and root execution.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ format: { type: 'string', enum: ['report', 'json'], description: 'Output format: "report" (Markdown scorecard, default) or "json" (raw result)' },
152
+ },
153
+ },
154
+ },
143
155
  ];
144
156
  // ===== Tool Execution =====
145
157
  function executeTool(name, args) {
@@ -263,6 +275,30 @@ function executeTool(name, args) {
263
275
  ],
264
276
  };
265
277
  }
278
+ case 'compliance_check': {
279
+ const report = runComplianceAudit(guard.config);
280
+ if (args.format === 'json') {
281
+ return {
282
+ score: report.score,
283
+ grade: report.grade,
284
+ passed: report.passed,
285
+ warned: report.warned,
286
+ failed: report.failed,
287
+ manual: report.manual,
288
+ total: report.total,
289
+ results: report.results.map(r => ({
290
+ id: r.control.id,
291
+ regulation: r.control.regulation,
292
+ article: r.control.article,
293
+ title: guard.locale === 'zh' ? r.control.title_zh : r.control.title_en,
294
+ status: r.status,
295
+ detail: guard.locale === 'zh' ? r.detail_zh : r.detail_en,
296
+ })),
297
+ };
298
+ }
299
+ // default: return Markdown report as text
300
+ return { report: renderComplianceReport(report, guard.locale) };
301
+ }
266
302
  default:
267
303
  throw new Error(`Unknown tool: ${name}`);
268
304
  }
@@ -0,0 +1,27 @@
1
+ export interface DomesticModel {
2
+ id: string;
3
+ name_zh: string;
4
+ name_en: string;
5
+ vendor_zh: string;
6
+ /** OpenAI 兼容 base_url(若支持) */
7
+ baseUrl: string;
8
+ /** 是否提供 OpenAI 兼容接口(决定迁移难度) */
9
+ openaiCompatible: boolean;
10
+ }
11
+ /** 境内主流大模型(均为境内可备案/合规部署,按知名度排序) */
12
+ export declare const DOMESTIC_MODELS: DomesticModel[];
13
+ export interface DomesticSuggestion {
14
+ /** 触发的境外厂商(中文) */
15
+ overseas_zh: string;
16
+ overseas_en: string;
17
+ /** 迁移难度 */
18
+ difficulty_zh: string;
19
+ difficulty_en: string;
20
+ /** 推荐的境内替代(取兼容优先的前几个) */
21
+ alternatives: DomesticModel[];
22
+ }
23
+ /**
24
+ * 针对某个境外厂商给出境内替代建议。
25
+ * @param key endpointId(如 'openai')或 provider_en(如 'OpenAI')
26
+ */
27
+ export declare function suggestDomestic(key: string, provider_zh?: string, provider_en?: string): DomesticSuggestion;
@@ -0,0 +1,62 @@
1
+ // src/rules/domestic-alternatives.ts — 境内已备案大模型替代建议
2
+ //
3
+ // 扫到境外大模型(端点/SDK 依赖)后,给出可执行的「境内合规替代」处方:
4
+ // 把"你有数据出境风险"变成"换成这个、这样换"。这是 ShellWard 面向中国
5
+ // 市场最具差异化、最可执行的一环 —— 英文工具不会做。
6
+ //
7
+ // 杀手锏:境内主流模型多数提供 **OpenAI 兼容接口**,对 `openai` SDK 的项目
8
+ // 往往只需改 base_url + api key、代码零改动即可迁移到合规模型。
9
+ //
10
+ // 注:base_url 为各厂商公开的 OpenAI 兼容端点(可能随官方调整,迁移前以官方文档为准)。
11
+ /** 境内主流大模型(均为境内可备案/合规部署,按知名度排序) */
12
+ export const DOMESTIC_MODELS = [
13
+ {
14
+ id: 'qwen', name_zh: '通义千问', name_en: 'Qwen', vendor_zh: '阿里云百炼/DashScope',
15
+ baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', openaiCompatible: true,
16
+ },
17
+ {
18
+ id: 'deepseek', name_zh: 'DeepSeek', name_en: 'DeepSeek', vendor_zh: '深度求索',
19
+ baseUrl: 'https://api.deepseek.com', openaiCompatible: true,
20
+ },
21
+ {
22
+ id: 'kimi', name_zh: 'Kimi', name_en: 'Kimi (Moonshot)', vendor_zh: '月之暗面',
23
+ baseUrl: 'https://api.moonshot.cn/v1', openaiCompatible: true,
24
+ },
25
+ {
26
+ id: 'glm', name_zh: '智谱 GLM', name_en: 'Zhipu GLM', vendor_zh: '智谱 AI',
27
+ baseUrl: 'https://open.bigmodel.cn/api/paas/v4', openaiCompatible: true,
28
+ },
29
+ {
30
+ id: 'doubao', name_zh: '豆包', name_en: 'Doubao', vendor_zh: '字节火山方舟',
31
+ baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', openaiCompatible: true,
32
+ },
33
+ {
34
+ id: 'ernie', name_zh: '文心一言', name_en: 'ERNIE', vendor_zh: '百度千帆',
35
+ baseUrl: 'https://qianfan.baidubce.com/v2', openaiCompatible: true,
36
+ },
37
+ ];
38
+ // 哪些境外厂商走 OpenAI 兼容协议(其 SDK 项目可零代码迁移到境内兼容端点)
39
+ const OPENAI_PROTOCOL = new Set(['openai', 'azure-openai', 'groq', 'together', 'mistral', 'perplexity', 'openrouter', 'xai']);
40
+ /**
41
+ * 针对某个境外厂商给出境内替代建议。
42
+ * @param key endpointId(如 'openai')或 provider_en(如 'OpenAI')
43
+ */
44
+ export function suggestDomestic(key, provider_zh, provider_en) {
45
+ const k = key.toLowerCase();
46
+ const isOpenAiProtocol = OPENAI_PROTOCOL.has(k) || /openai/.test(k);
47
+ // 推荐:OpenAI 兼容的境内模型优先(迁移最省事)
48
+ const alternatives = DOMESTIC_MODELS.filter(m => m.openaiCompatible).slice(0, 4);
49
+ const difficulty_zh = isOpenAiProtocol
50
+ ? '低 — 多为 OpenAI 兼容协议,通常只需改 base_url + API key,代码零改动'
51
+ : '中 — SDK 不同,建议改用境内模型的 OpenAI 兼容端点并调整调用代码';
52
+ const difficulty_en = isOpenAiProtocol
53
+ ? 'Low — OpenAI-compatible; usually just swap base_url + API key, no code change'
54
+ : 'Medium — different SDK; switch to a domestic OpenAI-compatible endpoint and adjust calls';
55
+ return {
56
+ overseas_zh: provider_zh || key,
57
+ overseas_en: provider_en || key,
58
+ difficulty_zh,
59
+ difficulty_en,
60
+ alternatives,
61
+ };
62
+ }
@@ -0,0 +1,37 @@
1
+ export interface OverseasEndpoint {
2
+ id: string;
3
+ /** 主机名匹配(小写、去端口)。命中其一即视为境外大模型端点 */
4
+ hosts: string[];
5
+ provider_zh: string;
6
+ provider_en: string;
7
+ }
8
+ export declare const OVERSEAS_LLM_ENDPOINTS: OverseasEndpoint[];
9
+ export interface OverseasMatch {
10
+ isOverseas: boolean;
11
+ endpointId?: string;
12
+ provider_zh?: string;
13
+ provider_en?: string;
14
+ host?: string;
15
+ }
16
+ /**
17
+ * 从任意字符串(URL / base_url / 命令行 / 配置)中提取主机名并判断是否境外大模型端点。
18
+ * 同时识别裸 URL 与命令行里的 https://... 形式。
19
+ */
20
+ export declare function detectOverseasLLM(text: string): OverseasMatch;
21
+ /** 包名(小写)→ 厂商 */
22
+ export declare const OVERSEAS_LLM_PACKAGES: Record<string, {
23
+ zh: string;
24
+ en: string;
25
+ }>;
26
+ export interface DepMatch {
27
+ pkg: string;
28
+ provider_zh: string;
29
+ provider_en: string;
30
+ }
31
+ /** 依赖清单文件名(小写)判定 */
32
+ export declare function isDependencyManifest(filename: string): boolean;
33
+ /**
34
+ * 从依赖清单内容中提取境外大模型 SDK 依赖。
35
+ * 对 package.json 解析 JSON 的 deps/devDeps/peerDeps;其余按"出现包名"宽松匹配。
36
+ */
37
+ export declare function detectOverseasDeps(filename: string, content: string): DepMatch[];
@@ -0,0 +1,147 @@
1
+ // src/rules/overseas-llm.ts — 境外大模型端点识别(数据出境检测)
2
+ //
3
+ // 中国差异化能力:识别请求是否指向「境外大模型 / 境外 AI 服务」端点。
4
+ // 在中国监管下,向境外大模型 API 发送个人信息/重要数据 = 数据出境,受
5
+ // 《促进数据跨境流动规定》《数据出境安全评估办法》约束。英文工具没有
6
+ // "出境"概念,这是 ShellWard 面向中国市场的护城河之一。
7
+ //
8
+ // 用途:
9
+ // - 体检引擎据此判断"是否存在数据出境风险"
10
+ // - 网关层据此标记"数据出境"事件 / 触发出境前脱敏(路线图)
11
+ //
12
+ // 边界:仅做端点归属判断(不做 IP 归属解析),覆盖主流境外大模型与聚合网关。
13
+ export const OVERSEAS_LLM_ENDPOINTS = [
14
+ { id: 'openai', hosts: ['api.openai.com', 'openai.com'], provider_zh: 'OpenAI', provider_en: 'OpenAI' },
15
+ { id: 'anthropic', hosts: ['api.anthropic.com', 'anthropic.com'], provider_zh: 'Anthropic Claude', provider_en: 'Anthropic Claude' },
16
+ { id: 'google', hosts: ['generativelanguage.googleapis.com', 'aiplatform.googleapis.com'], provider_zh: 'Google Gemini', provider_en: 'Google Gemini' },
17
+ { id: 'azure-openai', hosts: ['openai.azure.com'], provider_zh: 'Azure OpenAI', provider_en: 'Azure OpenAI' },
18
+ { id: 'aws-bedrock', hosts: ['bedrock-runtime', 'bedrock.', 'amazonaws.com'], provider_zh: 'AWS Bedrock', provider_en: 'AWS Bedrock' },
19
+ { id: 'cohere', hosts: ['api.cohere.ai', 'api.cohere.com'], provider_zh: 'Cohere', provider_en: 'Cohere' },
20
+ { id: 'mistral', hosts: ['api.mistral.ai'], provider_zh: 'Mistral AI', provider_en: 'Mistral AI' },
21
+ { id: 'groq', hosts: ['api.groq.com'], provider_zh: 'Groq', provider_en: 'Groq' },
22
+ { id: 'together', hosts: ['api.together.xyz', 'api.together.ai'], provider_zh: 'Together AI', provider_en: 'Together AI' },
23
+ { id: 'perplexity', hosts: ['api.perplexity.ai'], provider_zh: 'Perplexity', provider_en: 'Perplexity' },
24
+ { id: 'openrouter', hosts: ['openrouter.ai'], provider_zh: 'OpenRouter (聚合网关)', provider_en: 'OpenRouter (gateway)' },
25
+ { id: 'huggingface', hosts: ['api-inference.huggingface.co', 'huggingface.co'], provider_zh: 'HuggingFace', provider_en: 'HuggingFace' },
26
+ { id: 'xai', hosts: ['api.x.ai'], provider_zh: 'xAI Grok', provider_en: 'xAI Grok' },
27
+ ];
28
+ /**
29
+ * 从任意字符串(URL / base_url / 命令行 / 配置)中提取主机名并判断是否境外大模型端点。
30
+ * 同时识别裸 URL 与命令行里的 https://... 形式。
31
+ */
32
+ export function detectOverseasLLM(text) {
33
+ if (!text)
34
+ return { isOverseas: false };
35
+ const lower = text.toLowerCase();
36
+ // 提取所有候选主机名:URL 中的 host,或文本中出现的端点关键字
37
+ const hosts = extractHosts(lower);
38
+ for (const ep of OVERSEAS_LLM_ENDPOINTS) {
39
+ for (const h of ep.hosts) {
40
+ // host 列表里带 '.' 或子串形式(如 'bedrock.')做包含匹配,纯域名做后缀/相等匹配
41
+ const hit = hosts.some(host => host === h || host.endsWith('.' + h) || host.includes(h))
42
+ || lower.includes('://' + h)
43
+ || lower.includes(h);
44
+ if (hit) {
45
+ return {
46
+ isOverseas: true,
47
+ endpointId: ep.id,
48
+ provider_zh: ep.provider_zh,
49
+ provider_en: ep.provider_en,
50
+ host: h,
51
+ };
52
+ }
53
+ }
54
+ }
55
+ return { isOverseas: false };
56
+ }
57
+ // ===== 境外大模型 SDK 依赖检测 =====
58
+ // 几乎每个国产 AI 项目的依赖清单里都躺着 openai/anthropic 等境外 SDK —— 这是
59
+ // 一条"数据出境通道",命中率远高于扫 URL 字符串。英文工具不会把它框定为"出境"。
60
+ /** 包名(小写)→ 厂商 */
61
+ export const OVERSEAS_LLM_PACKAGES = {
62
+ // npm
63
+ 'openai': { zh: 'OpenAI', en: 'OpenAI' },
64
+ '@anthropic-ai/sdk': { zh: 'Anthropic Claude', en: 'Anthropic Claude' },
65
+ '@google/generative-ai': { zh: 'Google Gemini', en: 'Google Gemini' },
66
+ '@google-cloud/aiplatform': { zh: 'Google Vertex AI', en: 'Google Vertex AI' },
67
+ 'cohere-ai': { zh: 'Cohere', en: 'Cohere' },
68
+ '@mistralai/mistralai': { zh: 'Mistral AI', en: 'Mistral AI' },
69
+ 'groq-sdk': { zh: 'Groq', en: 'Groq' },
70
+ '@huggingface/inference': { zh: 'HuggingFace', en: 'HuggingFace' },
71
+ 'replicate': { zh: 'Replicate', en: 'Replicate' },
72
+ '@langchain/openai': { zh: 'OpenAI (LangChain)', en: 'OpenAI (LangChain)' },
73
+ '@langchain/anthropic': { zh: 'Anthropic (LangChain)', en: 'Anthropic (LangChain)' },
74
+ '@langchain/google-genai': { zh: 'Gemini (LangChain)', en: 'Gemini (LangChain)' },
75
+ // python
76
+ 'anthropic': { zh: 'Anthropic Claude', en: 'Anthropic Claude' },
77
+ 'google-generativeai': { zh: 'Google Gemini', en: 'Google Gemini' },
78
+ 'google-cloud-aiplatform': { zh: 'Google Vertex AI', en: 'Google Vertex AI' },
79
+ 'cohere': { zh: 'Cohere', en: 'Cohere' },
80
+ 'mistralai': { zh: 'Mistral AI', en: 'Mistral AI' },
81
+ 'groq': { zh: 'Groq', en: 'Groq' },
82
+ 'together': { zh: 'Together AI', en: 'Together AI' },
83
+ 'huggingface-hub': { zh: 'HuggingFace', en: 'HuggingFace' },
84
+ 'langchain-openai': { zh: 'OpenAI (LangChain)', en: 'OpenAI (LangChain)' },
85
+ 'langchain-anthropic': { zh: 'Anthropic (LangChain)', en: 'Anthropic (LangChain)' },
86
+ 'langchain-google-genai': { zh: 'Gemini (LangChain)', en: 'Gemini (LangChain)' },
87
+ // go
88
+ 'github.com/sashabaranov/go-openai': { zh: 'OpenAI (Go)', en: 'OpenAI (Go)' },
89
+ 'github.com/anthropics/anthropic-sdk-go': { zh: 'Anthropic (Go)', en: 'Anthropic (Go)' },
90
+ };
91
+ /** 依赖清单文件名(小写)判定 */
92
+ export function isDependencyManifest(filename) {
93
+ const f = filename.toLowerCase();
94
+ return f === 'package.json' || f === 'requirements.txt' || f === 'pyproject.toml' || f === 'go.mod';
95
+ }
96
+ /**
97
+ * 从依赖清单内容中提取境外大模型 SDK 依赖。
98
+ * 对 package.json 解析 JSON 的 deps/devDeps/peerDeps;其余按"出现包名"宽松匹配。
99
+ */
100
+ export function detectOverseasDeps(filename, content) {
101
+ const f = filename.toLowerCase();
102
+ const names = new Set();
103
+ if (f === 'package.json') {
104
+ try {
105
+ const json = JSON.parse(content);
106
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
107
+ const deps = json[field];
108
+ if (deps && typeof deps === 'object') {
109
+ for (const k of Object.keys(deps))
110
+ names.add(k.toLowerCase());
111
+ }
112
+ }
113
+ }
114
+ catch { /* 解析失败则跳过 */ }
115
+ }
116
+ else {
117
+ // requirements.txt / pyproject.toml / go.mod:按行宽松提取包名 token
118
+ for (const raw of content.split('\n')) {
119
+ const line = raw.trim().toLowerCase();
120
+ if (!line || line.startsWith('#') || line.startsWith('//'))
121
+ continue;
122
+ // 提取依赖名:requirements `pkg==x` / pyproject `"pkg>=x"` / go.mod `module vX`
123
+ const tokens = line.match(/[a-z0-9_.\-\/@]+/g) || [];
124
+ for (const t of tokens)
125
+ names.add(t);
126
+ }
127
+ }
128
+ const matches = [];
129
+ const seen = new Set();
130
+ for (const [pkg, prov] of Object.entries(OVERSEAS_LLM_PACKAGES)) {
131
+ if (names.has(pkg.toLowerCase()) && !seen.has(pkg)) {
132
+ seen.add(pkg);
133
+ matches.push({ pkg, provider_zh: prov.zh, provider_en: prov.en });
134
+ }
135
+ }
136
+ return matches;
137
+ }
138
+ /** 从文本中粗提取主机名(URL host 段) */
139
+ function extractHosts(lowerText) {
140
+ const hosts = [];
141
+ const urlRe = /https?:\/\/([a-z0-9.\-]+)(?::\d+)?/g;
142
+ let m;
143
+ while ((m = urlRe.exec(lowerText)) !== null) {
144
+ hosts.push(m[1]);
145
+ }
146
+ return hosts;
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.6.1",
3
+ "version": "0.6.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": [
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "type": "module",
45
45
  "bin": {
46
- "shellward": "dist/mcp-server.js",
46
+ "shellward": "dist/cli.js",
47
47
  "shellward-mcp": "dist/mcp-server.js"
48
48
  },
49
49
  "main": "dist/index.js",
@@ -57,8 +57,9 @@
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",
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",
61
61
  "test:redos": "npx tsx test-redos.ts",
62
+ "test:compliance": "npx tsx test-compliance.ts",
62
63
  "test:integration": "npx tsx test-integration.ts",
63
64
  "test:edge": "npx tsx test-edge-cases.ts",
64
65
  "test:sdk": "npx tsx test-sdk.ts",