shellward 0.6.1 → 0.6.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 +78 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +144 -0
- package/dist/commands/compliance.d.ts +2 -0
- package/dist/commands/compliance.js +21 -0
- package/dist/commands/index.js +6 -2
- package/dist/compliance/audit.d.ts +57 -0
- package/dist/compliance/audit.js +234 -0
- package/dist/compliance/project-scan.d.ts +23 -0
- package/dist/compliance/project-scan.js +225 -0
- package/dist/compliance/regulations.d.ts +35 -0
- package/dist/compliance/regulations.js +218 -0
- package/dist/compliance/report.d.ts +9 -0
- package/dist/compliance/report.js +167 -0
- package/dist/mcp-server.js +36 -0
- package/dist/rules/overseas-llm.d.ts +37 -0
- package/dist/rules/overseas-llm.js +147 -0
- package/package.json +4 -3
- package/src/cli.ts +154 -0
- package/src/commands/compliance.ts +25 -0
- package/src/commands/index.ts +6 -2
- package/src/compliance/audit.ts +310 -0
- package/src/compliance/project-scan.ts +263 -0
- package/src/compliance/regulations.ts +260 -0
- package/src/compliance/report.ts +189 -0
- package/src/mcp-server.ts +37 -0
- package/src/rules/overseas-llm.ts +174 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/compliance/project-scan.ts — 项目真实风险扫描
|
|
2
|
+
//
|
|
3
|
+
// 体检的灵魂:报告的是「用户项目的真实风险」,不是「ShellWard 的开关」。
|
|
4
|
+
// 零依赖遍历当前项目目录,找出可截图、可定位 (文件:行) 的合规风险:
|
|
5
|
+
// ① 境外大模型端点(硬编码 base_url/key)→ 数据出境风险
|
|
6
|
+
// ② 硬编码密钥(API key / 私钥 / 口令 / 连接串)
|
|
7
|
+
// ③ 文件中的中文 PII(身份证 / 手机号 / 银行卡)
|
|
8
|
+
// ④ .env 等敏感文件权限过宽
|
|
9
|
+
import { readdirSync, statSync, readFileSync } from 'fs';
|
|
10
|
+
import { join, relative, basename } from 'path';
|
|
11
|
+
import { detectOverseasLLM, detectOverseasDeps, isDependencyManifest } from '../rules/overseas-llm.js';
|
|
12
|
+
import { SENSITIVE_PATTERNS } from '../rules/sensitive-patterns.js';
|
|
13
|
+
// ===== 扫描边界(零依赖、可控) =====
|
|
14
|
+
const SKIP_DIRS = new Set([
|
|
15
|
+
'node_modules', '.git', 'dist', 'build', '.next', 'out',
|
|
16
|
+
'vendor', 'coverage', '.venv', 'venv', '__pycache__', '.cache', 'target',
|
|
17
|
+
]);
|
|
18
|
+
const SCAN_EXT = new Set([
|
|
19
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
20
|
+
'.py', '.go', '.rb', '.java', '.php', '.rs',
|
|
21
|
+
'.json', '.yaml', '.yml', '.toml', '.ini', '.conf', '.sh', '.txt', '.csv',
|
|
22
|
+
]);
|
|
23
|
+
const MAX_FILES = 3000;
|
|
24
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
25
|
+
const MAX_FINDINGS_PER_FILE = 20;
|
|
26
|
+
const MAX_TOTAL_FINDINGS = 500;
|
|
27
|
+
// 密钥类 vs PII 类(按 sensitive-patterns 的 id 归类)
|
|
28
|
+
const SECRET_IDS = new Set([
|
|
29
|
+
'openai_key', 'anthropic_key', 'aws_access', 'github_token',
|
|
30
|
+
'generic_api_key', 'private_key', 'jwt', 'password', 'conn_string',
|
|
31
|
+
]);
|
|
32
|
+
const PII_IDS = new Set(['id_card_cn', 'phone_cn', 'bank_card_cn', 'ssn_us', 'credit_card']);
|
|
33
|
+
/** 扫描项目目录,返回真实风险发现 */
|
|
34
|
+
export function scanProject(root) {
|
|
35
|
+
const findings = [];
|
|
36
|
+
const state = { files: 0, truncated: false };
|
|
37
|
+
walk(root, root, findings, state);
|
|
38
|
+
const counts = { overseas: 0, secret: 0, pii: 0, 'env-perm': 0 };
|
|
39
|
+
for (const f of findings)
|
|
40
|
+
counts[f.kind]++;
|
|
41
|
+
return {
|
|
42
|
+
root,
|
|
43
|
+
filesScanned: state.files,
|
|
44
|
+
truncated: state.truncated,
|
|
45
|
+
findings,
|
|
46
|
+
counts,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function walk(dir, root, findings, state) {
|
|
50
|
+
if (state.files >= MAX_FILES || findings.length >= MAX_TOTAL_FINDINGS) {
|
|
51
|
+
state.truncated = true;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const name of entries) {
|
|
62
|
+
if (state.files >= MAX_FILES || findings.length >= MAX_TOTAL_FINDINGS) {
|
|
63
|
+
state.truncated = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (name.startsWith('.') && !name.startsWith('.env')) {
|
|
67
|
+
// 跳过隐藏文件/目录,但保留 .env*
|
|
68
|
+
if (name !== '.')
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const full = join(dir, name);
|
|
72
|
+
let st;
|
|
73
|
+
try {
|
|
74
|
+
st = statSync(full);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (st.isDirectory()) {
|
|
80
|
+
if (SKIP_DIRS.has(name))
|
|
81
|
+
continue;
|
|
82
|
+
walk(full, root, findings, state);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!st.isFile())
|
|
86
|
+
continue;
|
|
87
|
+
// .env 权限检查(任意大小)
|
|
88
|
+
if (/^\.env(\..+)?$/.test(name)) {
|
|
89
|
+
checkEnvPerm(full, root, st.mode, findings);
|
|
90
|
+
}
|
|
91
|
+
// 内容扫描:文本类扩展 + .env* + 依赖清单(含 go.mod),且大小受限
|
|
92
|
+
const isEnv = /^\.env(\..+)?$/.test(name);
|
|
93
|
+
const isDep = isDependencyManifest(name);
|
|
94
|
+
const ext = name.includes('.') ? name.slice(name.lastIndexOf('.')) : '';
|
|
95
|
+
if (!isEnv && !isDep && !SCAN_EXT.has(ext))
|
|
96
|
+
continue;
|
|
97
|
+
if (st.size > MAX_FILE_BYTES)
|
|
98
|
+
continue;
|
|
99
|
+
let content;
|
|
100
|
+
try {
|
|
101
|
+
content = readFileSync(full, 'utf-8');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
state.files++;
|
|
107
|
+
if (isDep)
|
|
108
|
+
scanDependencies(name, content, full, root, findings);
|
|
109
|
+
scanContent(content, full, root, findings);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function scanDependencies(name, content, full, root, findings) {
|
|
113
|
+
const deps = detectOverseasDeps(name, content);
|
|
114
|
+
if (deps.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
const file = rel(root, full);
|
|
117
|
+
const lines = content.split('\n');
|
|
118
|
+
for (const d of deps) {
|
|
119
|
+
if (findings.length >= MAX_TOTAL_FINDINGS)
|
|
120
|
+
break;
|
|
121
|
+
// 粗定位包名所在行
|
|
122
|
+
let line;
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
if (lines[i].toLowerCase().includes(d.pkg.toLowerCase())) {
|
|
125
|
+
line = i + 1;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
findings.push({
|
|
130
|
+
kind: 'overseas',
|
|
131
|
+
file,
|
|
132
|
+
line,
|
|
133
|
+
detail: `境外大模型 SDK 依赖: ${d.pkg} (${d.provider_zh}) — 项目内含数据出境通道,调用即可能构成出境`,
|
|
134
|
+
severity: 'high',
|
|
135
|
+
provider_zh: d.provider_zh,
|
|
136
|
+
provider_en: d.provider_en,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function checkEnvPerm(full, root, mode, findings) {
|
|
141
|
+
// 仅 POSIX 有意义;Windows 上 mode 不可靠,跳过
|
|
142
|
+
if (process.platform === 'win32')
|
|
143
|
+
return;
|
|
144
|
+
const perm = mode & 0o777;
|
|
145
|
+
if (perm > 0o600) {
|
|
146
|
+
findings.push({
|
|
147
|
+
kind: 'env-perm',
|
|
148
|
+
file: rel(root, full),
|
|
149
|
+
detail: `权限过宽 (${perm.toString(8)}),建议 chmod 600 — 含密钥的 .env 不应组/其他可读`,
|
|
150
|
+
severity: 'high',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function scanContent(content, full, root, findings) {
|
|
155
|
+
const file = rel(root, full);
|
|
156
|
+
const lines = content.split('\n');
|
|
157
|
+
let perFile = 0;
|
|
158
|
+
const dedup = new Set();
|
|
159
|
+
for (let i = 0; i < lines.length; i++) {
|
|
160
|
+
if (perFile >= MAX_FINDINGS_PER_FILE || findings.length >= MAX_TOTAL_FINDINGS)
|
|
161
|
+
break;
|
|
162
|
+
const line = lines[i];
|
|
163
|
+
if (!line || line.length > 4000)
|
|
164
|
+
continue;
|
|
165
|
+
// ① 境外大模型端点
|
|
166
|
+
const ov = detectOverseasLLM(line);
|
|
167
|
+
if (ov.isOverseas) {
|
|
168
|
+
const key = `overseas:${ov.endpointId}`;
|
|
169
|
+
if (!dedup.has(key)) {
|
|
170
|
+
dedup.add(key);
|
|
171
|
+
findings.push({
|
|
172
|
+
kind: 'overseas',
|
|
173
|
+
file,
|
|
174
|
+
line: i + 1,
|
|
175
|
+
detail: `境外大模型端点: ${ov.provider_zh} — 向其发送个人信息/重要数据即构成数据出境`,
|
|
176
|
+
severity: 'critical',
|
|
177
|
+
endpointId: ov.endpointId,
|
|
178
|
+
provider_zh: ov.provider_zh,
|
|
179
|
+
provider_en: ov.provider_en,
|
|
180
|
+
});
|
|
181
|
+
perFile++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ② / ③ 密钥与 PII
|
|
185
|
+
for (const pat of SENSITIVE_PATTERNS) {
|
|
186
|
+
if (perFile >= MAX_FINDINGS_PER_FILE)
|
|
187
|
+
break;
|
|
188
|
+
const isSecret = SECRET_IDS.has(pat.id);
|
|
189
|
+
const isPII = PII_IDS.has(pat.id);
|
|
190
|
+
if (!isSecret && !isPII)
|
|
191
|
+
continue;
|
|
192
|
+
const re = new RegExp(pat.regex.source, pat.regex.flags);
|
|
193
|
+
let m;
|
|
194
|
+
while ((m = re.exec(line)) !== null) {
|
|
195
|
+
if (pat.validate && !pat.validate(m[0]))
|
|
196
|
+
continue;
|
|
197
|
+
const key = `${pat.id}:${i}`;
|
|
198
|
+
if (dedup.has(key))
|
|
199
|
+
break;
|
|
200
|
+
dedup.add(key);
|
|
201
|
+
findings.push({
|
|
202
|
+
kind: isSecret ? 'secret' : 'pii',
|
|
203
|
+
file,
|
|
204
|
+
line: i + 1,
|
|
205
|
+
detail: isSecret
|
|
206
|
+
? `硬编码${pat.name}: ${preview(m[0])} — 凭据不应写入源码/配置`
|
|
207
|
+
: `${pat.name}: ${preview(m[0])} — 个人信息出现在文件中,需评估最小必要与脱敏`,
|
|
208
|
+
severity: isSecret ? 'critical' : 'high',
|
|
209
|
+
});
|
|
210
|
+
perFile++;
|
|
211
|
+
if (perFile >= MAX_FINDINGS_PER_FILE)
|
|
212
|
+
break;
|
|
213
|
+
if (!re.global)
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function preview(s) {
|
|
220
|
+
return s.length > 10 ? s.slice(0, 6) + '***' : s.slice(0, 3) + '***';
|
|
221
|
+
}
|
|
222
|
+
function rel(root, full) {
|
|
223
|
+
const r = relative(root, full);
|
|
224
|
+
return r || basename(full);
|
|
225
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** 控制项归属的法规 */
|
|
2
|
+
export type Regulation = 'CSL' | 'PIPL' | 'MLPS' | 'CBDT' | 'GENAI';
|
|
3
|
+
/** 控制项的检测方式 —— 决定体检引擎用哪种 checker */
|
|
4
|
+
export type CheckMethod = 'capability' | 'config' | 'audit' | 'env' | 'manual';
|
|
5
|
+
export type Severity = 'critical' | 'high' | 'medium' | 'low';
|
|
6
|
+
export interface ComplianceControl {
|
|
7
|
+
/** 稳定 ID,体检引擎据此映射 checker,报告据此引用 */
|
|
8
|
+
id: string;
|
|
9
|
+
regulation: Regulation;
|
|
10
|
+
/** 法规条款 / 国标编号 */
|
|
11
|
+
article: string;
|
|
12
|
+
title_zh: string;
|
|
13
|
+
title_en: string;
|
|
14
|
+
/** 监管要求原文要点 */
|
|
15
|
+
requirement_zh: string;
|
|
16
|
+
requirement_en: string;
|
|
17
|
+
method: CheckMethod;
|
|
18
|
+
severity: Severity;
|
|
19
|
+
/** 不满足时的整改建议 */
|
|
20
|
+
remediation_zh: string;
|
|
21
|
+
remediation_en: string;
|
|
22
|
+
}
|
|
23
|
+
/** 法规中文显示名 */
|
|
24
|
+
export declare const REGULATION_NAMES: Record<Regulation, {
|
|
25
|
+
zh: string;
|
|
26
|
+
en: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* 合规控制项清单。
|
|
30
|
+
* 注:本清单帮助企业"满足"合规技术要求,不等于"替企业完成"合规
|
|
31
|
+
* (备案 / 定级 / PIA 主体责任不可外包)。method='manual' 的项 ShellWard 仅提供举证支撑。
|
|
32
|
+
*/
|
|
33
|
+
export declare const COMPLIANCE_CONTROLS: ComplianceControl[];
|
|
34
|
+
/** 按法规分组 */
|
|
35
|
+
export declare function controlsByRegulation(): Record<Regulation, ComplianceControl[]>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// src/compliance/regulations.ts — 中国 AI 合规控制项映射层
|
|
2
|
+
//
|
|
3
|
+
// 把网安法 / PIPL / 等保2.0 / 数据出境 / 生成式AI标识 等法规的「可核查控制项」
|
|
4
|
+
// 结构化为统一数据模型,作为合规体检引擎 (compliance/audit.ts) 的数据源。
|
|
5
|
+
//
|
|
6
|
+
// 每条控制项 = 一条真实法规条款 → 一个可被 ShellWard 检测或支撑的技术点。
|
|
7
|
+
// 法规出处见 README「合规」章节与研究报告(cac.gov.cn / npc.gov.cn / 国标平台)。
|
|
8
|
+
/** 法规中文显示名 */
|
|
9
|
+
export const REGULATION_NAMES = {
|
|
10
|
+
CSL: { zh: '网络安全法 (2026.1.1)', en: 'Cybersecurity Law (2026-01-01)' },
|
|
11
|
+
PIPL: { zh: '个人信息保护法', en: 'PIPL' },
|
|
12
|
+
MLPS: { zh: '等保 2.0 (GB/T 22239)', en: 'MLPS 2.0' },
|
|
13
|
+
CBDT: { zh: '数据出境', en: 'Cross-Border Data Transfer' },
|
|
14
|
+
GENAI: { zh: '生成式AI / 内容标识', en: 'GenAI / Content Labeling' },
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 合规控制项清单。
|
|
18
|
+
* 注:本清单帮助企业"满足"合规技术要求,不等于"替企业完成"合规
|
|
19
|
+
* (备案 / 定级 / PIA 主体责任不可外包)。method='manual' 的项 ShellWard 仅提供举证支撑。
|
|
20
|
+
*/
|
|
21
|
+
export const COMPLIANCE_CONTROLS = [
|
|
22
|
+
// ===== 网络安全法 (CSL) =====
|
|
23
|
+
{
|
|
24
|
+
id: 'csl-audit-log',
|
|
25
|
+
regulation: 'CSL',
|
|
26
|
+
article: '第二十三条',
|
|
27
|
+
title_zh: '网络日志留存不少于 6 个月',
|
|
28
|
+
title_en: 'Retain network logs for ≥6 months',
|
|
29
|
+
requirement_zh: '采取监测、记录网络运行状态的技术措施,留存相关网络日志不少于六个月。',
|
|
30
|
+
requirement_en: 'Retain network operation logs for no less than six months.',
|
|
31
|
+
method: 'audit',
|
|
32
|
+
severity: 'high',
|
|
33
|
+
remediation_zh: '启用 ShellWard 审计日志,确保工具调用 / prompt / 决策链统一留痕、防篡改、保留 ≥6 个月。',
|
|
34
|
+
remediation_en: 'Enable ShellWard audit logging with ≥6-month, tamper-resistant retention.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'csl-content-block',
|
|
38
|
+
regulation: 'CSL',
|
|
39
|
+
article: '第四十九条',
|
|
40
|
+
title_zh: '发现禁止信息立即停止传输并留痕',
|
|
41
|
+
title_en: 'Stop transmission of prohibited info & keep records',
|
|
42
|
+
requirement_zh: '发现法律禁止发布或传输的信息,立即停止传输、采取处置措施、保存记录并报告。',
|
|
43
|
+
requirement_en: 'On detecting prohibited information, immediately stop transmission, dispose, record and report.',
|
|
44
|
+
method: 'capability',
|
|
45
|
+
severity: 'high',
|
|
46
|
+
remediation_zh: '启用输出内容审查层 (outputScanner / outboundGuard),对生成内容实时审查、阻断并留痕。',
|
|
47
|
+
remediation_en: 'Enable output content review layers to block and log prohibited content in real time.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'csl-intrusion',
|
|
51
|
+
regulation: 'CSL',
|
|
52
|
+
article: '第二十一条 / 等保',
|
|
53
|
+
title_zh: '入侵防范与异常监测',
|
|
54
|
+
title_en: 'Intrusion prevention & anomaly monitoring',
|
|
55
|
+
requirement_zh: '采取防范计算机病毒和网络攻击、网络侵入等危害网络安全行为的技术措施。',
|
|
56
|
+
requirement_en: 'Take technical measures against intrusion and attacks endangering network security.',
|
|
57
|
+
method: 'capability',
|
|
58
|
+
severity: 'medium',
|
|
59
|
+
remediation_zh: '启用提示注入检测 (inputAuditor) 与危险命令拦截 (toolBlocker)。',
|
|
60
|
+
remediation_en: 'Enable prompt-injection detection and dangerous-command blocking.',
|
|
61
|
+
},
|
|
62
|
+
// ===== 个人信息保护法 (PIPL) =====
|
|
63
|
+
{
|
|
64
|
+
id: 'pipl-spi-detect',
|
|
65
|
+
regulation: 'PIPL',
|
|
66
|
+
article: '第二十八条',
|
|
67
|
+
title_zh: '敏感个人信息识别 (7类+未成年人)',
|
|
68
|
+
title_en: 'Detect sensitive personal information',
|
|
69
|
+
requirement_zh: '识别生物识别、医疗健康、金融账户、行踪轨迹等敏感个人信息及不满14岁未成年人信息。',
|
|
70
|
+
requirement_en: 'Detect sensitive PI: biometrics, health, financial accounts, location, minors under 14.',
|
|
71
|
+
method: 'capability',
|
|
72
|
+
severity: 'critical',
|
|
73
|
+
remediation_zh: '启用 PII/敏感数据扫描层 (outputScanner),覆盖身份证、银行卡、手机号等中文敏感信息。',
|
|
74
|
+
remediation_en: 'Enable PII/SPI scanning covering Chinese ID, bank card, phone, etc.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'pipl-minimize',
|
|
78
|
+
regulation: 'PIPL',
|
|
79
|
+
article: '第六条 / 第十九条',
|
|
80
|
+
title_zh: '最小必要 + 数据流向管控',
|
|
81
|
+
title_en: 'Data minimization & flow control',
|
|
82
|
+
requirement_zh: '处理个人信息应限于实现处理目的的最小范围,非必要不收集、不外发。',
|
|
83
|
+
requirement_en: 'Process PI within the minimum scope necessary for the purpose.',
|
|
84
|
+
method: 'capability',
|
|
85
|
+
severity: 'high',
|
|
86
|
+
remediation_zh: '启用数据流追踪层 (dataFlowGuard):读取敏感数据后向外发送将被拦截。',
|
|
87
|
+
remediation_en: 'Enable data-flow tracking: outbound send is blocked after sensitive data access.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'pipl-pia',
|
|
91
|
+
regulation: 'PIPL',
|
|
92
|
+
article: '第五十五条 / 第五十六条',
|
|
93
|
+
title_zh: '个人信息保护影响评估 (PIA) 并留存 ≥3 年',
|
|
94
|
+
title_en: 'Conduct PIA & retain records ≥3 years',
|
|
95
|
+
requirement_zh: '处理敏感PI、自动化决策、对外提供、出境等情形须事前进行 PIA,报告留存至少 3 年。',
|
|
96
|
+
requirement_en: 'Conduct a PIA for sensitive PI / automated decisions / cross-border transfer; retain ≥3 years.',
|
|
97
|
+
method: 'audit',
|
|
98
|
+
severity: 'high',
|
|
99
|
+
remediation_zh: '由数据流入/出口触发 PIA 工作流并将报告纳入防篡改审计存储(≥3 年)。',
|
|
100
|
+
remediation_en: 'Trigger PIA workflow on data ingress/egress; store reports in tamper-resistant audit (≥3y).',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'pipl-auto-decision',
|
|
104
|
+
regulation: 'PIPL',
|
|
105
|
+
article: '第二十四条',
|
|
106
|
+
title_zh: '自动化决策记录 + 人工复核回退',
|
|
107
|
+
title_en: 'Automated-decision logging & human-in-the-loop',
|
|
108
|
+
requirement_zh: '自动化决策应保证透明、结果公平,并提供拒绝纯自动化决策、要求说明的途径。',
|
|
109
|
+
requirement_en: 'Ensure transparency for automated decisions and a human-review fallback.',
|
|
110
|
+
method: 'capability',
|
|
111
|
+
severity: 'medium',
|
|
112
|
+
remediation_zh: '记录自动化决策调用(输入特征/模型版本/输出),高风险动作转人工复核 (securityGate)。',
|
|
113
|
+
remediation_en: 'Log automated decisions and route high-risk actions to human review.',
|
|
114
|
+
},
|
|
115
|
+
// ===== 等保 2.0 (MLPS) =====
|
|
116
|
+
{
|
|
117
|
+
id: 'mlps-audit-fields',
|
|
118
|
+
regulation: 'MLPS',
|
|
119
|
+
article: 'GB/T 22239 8.1.4.3',
|
|
120
|
+
title_zh: '安全审计:覆盖每用户、记录五要素',
|
|
121
|
+
title_en: 'Security audit: per-user, five-element records',
|
|
122
|
+
requirement_zh: '审计覆盖每个用户,记录时间、用户、类型、成败及其他相关信息,审计记录受保护防中断。',
|
|
123
|
+
requirement_en: 'Audit each user; record time, user, type, result and other info; protect audit records.',
|
|
124
|
+
method: 'audit',
|
|
125
|
+
severity: 'high',
|
|
126
|
+
remediation_zh: '使用 ShellWard 审计日志记录五要素并集中防篡改存储。',
|
|
127
|
+
remediation_en: 'Use ShellWard audit log to record five elements with tamper-resistant storage.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'mlps-access-control',
|
|
131
|
+
regulation: 'MLPS',
|
|
132
|
+
article: 'GB/T 22239 8.1.4.2',
|
|
133
|
+
title_zh: '访问控制:最小权限 + 工具/数据粒度',
|
|
134
|
+
title_en: 'Access control: least privilege & granularity',
|
|
135
|
+
requirement_zh: '按最小权限分配,访问控制粒度达到用户/进程级及文件/数据库表级。',
|
|
136
|
+
requirement_en: 'Assign least privilege; control granularity to user/process and file/table level.',
|
|
137
|
+
method: 'capability',
|
|
138
|
+
severity: 'medium',
|
|
139
|
+
remediation_zh: '启用工具策略层 (securityGate),对高风险工具/资源按最小授权管控。',
|
|
140
|
+
remediation_en: 'Enable tool-policy gate with least-privilege control over high-risk tools.',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'mlps-not-root',
|
|
144
|
+
regulation: 'MLPS',
|
|
145
|
+
article: 'GB/T 22239 8.1.x',
|
|
146
|
+
title_zh: '不以特权账户 (root) 运行',
|
|
147
|
+
title_en: 'Do not run as privileged (root) account',
|
|
148
|
+
requirement_zh: '应遵循最小化原则,避免以最高权限账户长期运行业务进程。',
|
|
149
|
+
requirement_en: 'Follow minimization; avoid running business processes as the root account.',
|
|
150
|
+
method: 'env',
|
|
151
|
+
severity: 'medium',
|
|
152
|
+
remediation_zh: '使用普通用户运行 + 容器隔离,避免以 root 启动 Agent。',
|
|
153
|
+
remediation_en: 'Run as non-root user with container isolation.',
|
|
154
|
+
},
|
|
155
|
+
// ===== 数据出境 (CBDT) =====
|
|
156
|
+
{
|
|
157
|
+
id: 'cbdt-overseas-llm',
|
|
158
|
+
regulation: 'CBDT',
|
|
159
|
+
article: '促进数据跨境流动规定',
|
|
160
|
+
title_zh: '境外大模型调用 = 数据出境识别',
|
|
161
|
+
title_en: 'Detect overseas LLM calls as data export',
|
|
162
|
+
requirement_zh: '向境外接收方提供个人信息/重要数据须识别并走相应路径;重要数据一律不得无评估出境。',
|
|
163
|
+
requirement_en: 'Identify PI/important-data export to overseas recipients; important data needs assessment.',
|
|
164
|
+
method: 'env',
|
|
165
|
+
severity: 'critical',
|
|
166
|
+
remediation_zh: '识别请求目的地是否境外大模型端点 (OpenAI/Anthropic/Gemini 等),标记"数据出境"事件;含敏感数据应路由境内已备案模型或先脱敏。',
|
|
167
|
+
remediation_en: 'Detect overseas LLM endpoints, flag data-export events; route sensitive data to domestic models or de-identify first.',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'cbdt-redact-before-export',
|
|
171
|
+
regulation: 'CBDT',
|
|
172
|
+
article: '安全评估 / 标准合同',
|
|
173
|
+
title_zh: '出境前脱敏 / 防裸数据出境',
|
|
174
|
+
title_en: 'De-identify before export / block raw export',
|
|
175
|
+
requirement_zh: '出境数据应最小化并采取加密、去标识化等安全措施,防止敏感个人信息裸数据出境。',
|
|
176
|
+
requirement_en: 'Minimize and de-identify export data; prevent raw sensitive PI from leaving the border.',
|
|
177
|
+
method: 'capability',
|
|
178
|
+
severity: 'high',
|
|
179
|
+
remediation_zh: '启用数据流追踪 + 出境拦截:检测到敏感数据流向境外端点时阻断或脱敏。',
|
|
180
|
+
remediation_en: 'Enable data-flow tracking + export interception to block or de-identify sensitive egress.',
|
|
181
|
+
},
|
|
182
|
+
// ===== 生成式AI / 内容标识 (GENAI) =====
|
|
183
|
+
{
|
|
184
|
+
id: 'genai-label',
|
|
185
|
+
regulation: 'GENAI',
|
|
186
|
+
article: '标识办法 / GB 45438-2025',
|
|
187
|
+
title_zh: 'AI生成内容标识 (显式 + 元数据)',
|
|
188
|
+
title_en: 'Label AI-generated content (explicit + metadata)',
|
|
189
|
+
requirement_zh: '生成合成内容须添加用户可感知的显式标识及文件元数据隐式标识 (XMP / TC260 命名空间)。',
|
|
190
|
+
requirement_en: 'Add user-visible explicit labels and metadata implicit labels (XMP / TC260 namespace).',
|
|
191
|
+
method: 'manual',
|
|
192
|
+
severity: 'high',
|
|
193
|
+
remediation_zh: '在输出层追加"AI生成"显式标识,导出文件按 GB 45438 写入 XMP 元数据 7 字段(路线图功能)。',
|
|
194
|
+
remediation_en: 'Append explicit "AI-generated" labels; write GB 45438 XMP metadata on export (roadmap).',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'genai-content-safety',
|
|
198
|
+
regulation: 'GENAI',
|
|
199
|
+
article: '暂行办法第四条 / 安全基本要求',
|
|
200
|
+
title_zh: '内容安全过滤 (违禁类别 + 拒答率)',
|
|
201
|
+
title_en: 'Content safety filtering (prohibited categories)',
|
|
202
|
+
requirement_zh: '生成内容不得含违法不良信息;安全评估要求应拒答率 ≥95%、误拒率 ≤5%。',
|
|
203
|
+
requirement_en: 'Generated content must exclude prohibited info; refusal rate ≥95%, false-refusal ≤5%.',
|
|
204
|
+
method: 'manual',
|
|
205
|
+
severity: 'high',
|
|
206
|
+
remediation_zh: '接入内容安全过滤引擎对标 31 类违禁内容(路线图功能,可对接国产审核 API)。',
|
|
207
|
+
remediation_en: 'Integrate content-safety filtering for the 31 prohibited categories (roadmap).',
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
/** 按法规分组 */
|
|
211
|
+
export function controlsByRegulation() {
|
|
212
|
+
const out = {};
|
|
213
|
+
for (const c of COMPLIANCE_CONTROLS) {
|
|
214
|
+
;
|
|
215
|
+
(out[c.regulation] ||= []).push(c);
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ComplianceReport } from './audit.js';
|
|
2
|
+
import type { ProjectScanResult } from './project-scan.js';
|
|
3
|
+
/** 渲染合规体检报告为 Markdown。locale 决定中英文。 */
|
|
4
|
+
export declare function renderComplianceReport(report: ComplianceReport, locale: 'zh' | 'en'): string;
|
|
5
|
+
/**
|
|
6
|
+
* 渲染「项目实测风险」段 —— 关于用户项目的真实发现 (文件:行),
|
|
7
|
+
* 这是评分卡可截图、可信的关键。附加在合规报告之后。
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderProjectFindings(scan: ProjectScanResult, locale: 'zh' | 'en'): string;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/compliance/report.ts — 合规体检报告 Markdown 渲染
|
|
2
|
+
//
|
|
3
|
+
// 把 ComplianceReport 渲染成红黄绿评分卡(可截图传播 = 月1 获客钩子)。
|
|
4
|
+
// 按法规分组,每项给状态图标 + 结论 + 整改建议。
|
|
5
|
+
import { REGULATION_NAMES } from './regulations.js';
|
|
6
|
+
const STATUS_ICON = {
|
|
7
|
+
pass: '🟢',
|
|
8
|
+
warn: '🟡',
|
|
9
|
+
fail: '🔴',
|
|
10
|
+
manual: '⚪',
|
|
11
|
+
};
|
|
12
|
+
const STATUS_TEXT_ZH = {
|
|
13
|
+
pass: '合规', warn: '部分', fail: '不合规', manual: '待确认',
|
|
14
|
+
};
|
|
15
|
+
const STATUS_TEXT_EN = {
|
|
16
|
+
pass: 'Pass', warn: 'Partial', fail: 'Fail', manual: 'Manual',
|
|
17
|
+
};
|
|
18
|
+
const REGULATION_ORDER = ['CSL', 'PIPL', 'MLPS', 'CBDT', 'GENAI'];
|
|
19
|
+
/** 渲染合规体检报告为 Markdown。locale 决定中英文。 */
|
|
20
|
+
export function renderComplianceReport(report, locale) {
|
|
21
|
+
const zh = locale === 'zh';
|
|
22
|
+
const L = [];
|
|
23
|
+
// ===== 标题 + 总评分卡 =====
|
|
24
|
+
L.push(zh ? '# 🛡️ AI 应用合规体检报告' : '# 🛡️ AI Application Compliance Report');
|
|
25
|
+
L.push('');
|
|
26
|
+
L.push(zh
|
|
27
|
+
? `> ShellWard 合规网关自动生成 · ${report.generatedAt.slice(0, 19).replace('T', ' ')} UTC`
|
|
28
|
+
: `> Generated by ShellWard Compliance Gateway · ${report.generatedAt.slice(0, 19).replace('T', ' ')} UTC`);
|
|
29
|
+
L.push('');
|
|
30
|
+
const bar = scoreBar(report.score);
|
|
31
|
+
L.push(zh ? '## 总评' : '## Overall');
|
|
32
|
+
L.push('');
|
|
33
|
+
L.push(`**${zh ? '合规得分' : 'Score'}: ${report.score}/100 ${gradeBadge(report.grade)}**`);
|
|
34
|
+
L.push('');
|
|
35
|
+
L.push('```');
|
|
36
|
+
L.push(`${bar} ${report.score}/100 [${report.grade}]`);
|
|
37
|
+
L.push('```');
|
|
38
|
+
L.push('');
|
|
39
|
+
L.push(zh
|
|
40
|
+
? `🟢 合规 ${report.passed} | 🟡 部分 ${report.warned} | 🔴 不合规 ${report.failed} | ⚪ 待确认 ${report.manual} (共 ${report.total} 项)`
|
|
41
|
+
: `🟢 Pass ${report.passed} | 🟡 Partial ${report.warned} | 🔴 Fail ${report.failed} | ⚪ Manual ${report.manual} (${report.total} controls)`);
|
|
42
|
+
if (report.projectPenalty && report.projectPenalty > 0) {
|
|
43
|
+
L.push('');
|
|
44
|
+
L.push(zh
|
|
45
|
+
? `> 含项目实测风险扣分 **-${report.projectPenalty}**(见下方「项目实测风险」)`
|
|
46
|
+
: `> Includes **-${report.projectPenalty}** from project scan findings (see Project Scan Findings)`);
|
|
47
|
+
}
|
|
48
|
+
L.push('');
|
|
49
|
+
// ===== 优先整改(fail 项,按严重度) =====
|
|
50
|
+
const fails = report.results
|
|
51
|
+
.filter(r => r.status === 'fail')
|
|
52
|
+
.sort((a, b) => severityRank(b) - severityRank(a));
|
|
53
|
+
if (fails.length > 0) {
|
|
54
|
+
L.push(zh ? '## ⚠️ 优先整改项' : '## ⚠️ Priority Fixes');
|
|
55
|
+
L.push('');
|
|
56
|
+
for (const r of fails) {
|
|
57
|
+
L.push(`- 🔴 **${zh ? r.control.title_zh : r.control.title_en}** `
|
|
58
|
+
+ `(${regName(r.control.regulation, zh)} ${r.control.article})`);
|
|
59
|
+
L.push(` - ${zh ? '整改' : 'Fix'}: ${zh ? r.control.remediation_zh : r.control.remediation_en}`);
|
|
60
|
+
}
|
|
61
|
+
L.push('');
|
|
62
|
+
}
|
|
63
|
+
// ===== 按法规分组明细 =====
|
|
64
|
+
L.push(zh ? '## 分项明细' : '## Detailed Results');
|
|
65
|
+
L.push('');
|
|
66
|
+
const grouped = groupByRegulation(report.results);
|
|
67
|
+
for (const reg of REGULATION_ORDER) {
|
|
68
|
+
const items = grouped[reg];
|
|
69
|
+
if (!items || items.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
L.push(`### ${regName(reg, zh)}`);
|
|
72
|
+
L.push('');
|
|
73
|
+
L.push(zh
|
|
74
|
+
? '| 状态 | 控制项 | 条款 | 结论 |'
|
|
75
|
+
: '| Status | Control | Article | Result |');
|
|
76
|
+
L.push('|---|---|---|---|');
|
|
77
|
+
for (const r of items) {
|
|
78
|
+
const icon = STATUS_ICON[r.status];
|
|
79
|
+
const txt = zh ? STATUS_TEXT_ZH[r.status] : STATUS_TEXT_EN[r.status];
|
|
80
|
+
const title = zh ? r.control.title_zh : r.control.title_en;
|
|
81
|
+
const detail = (zh ? r.detail_zh : r.detail_en).replace(/\|/g, '/').replace(/\n/g, ' ');
|
|
82
|
+
L.push(`| ${icon} ${txt} | ${title} | ${r.control.article} | ${detail} |`);
|
|
83
|
+
}
|
|
84
|
+
L.push('');
|
|
85
|
+
}
|
|
86
|
+
// ===== 免责声明 =====
|
|
87
|
+
L.push('---');
|
|
88
|
+
L.push(zh
|
|
89
|
+
? '> **说明**:本报告帮助评估并满足合规技术要求,不构成法律意见,亦不替代算法备案/定级备案/PIA 等主体责任。⚪ 待确认项需结合业务人工判定。'
|
|
90
|
+
: '> **Note**: This report assists with technical compliance and is not legal advice. ⚪ items require manual review.');
|
|
91
|
+
return L.join('\n');
|
|
92
|
+
}
|
|
93
|
+
const KIND_LABEL = {
|
|
94
|
+
overseas: { zh: '数据出境风险', en: 'Data export risk', icon: '🌐' },
|
|
95
|
+
secret: { zh: '硬编码密钥', en: 'Hardcoded secret', icon: '🔑' },
|
|
96
|
+
pii: { zh: '个人信息暴露', en: 'PII exposure', icon: '🪪' },
|
|
97
|
+
'env-perm': { zh: '.env 权限', en: '.env permission', icon: '📂' },
|
|
98
|
+
};
|
|
99
|
+
const KIND_ORDER = ['overseas', 'secret', 'pii', 'env-perm'];
|
|
100
|
+
/**
|
|
101
|
+
* 渲染「项目实测风险」段 —— 关于用户项目的真实发现 (文件:行),
|
|
102
|
+
* 这是评分卡可截图、可信的关键。附加在合规报告之后。
|
|
103
|
+
*/
|
|
104
|
+
export function renderProjectFindings(scan, locale) {
|
|
105
|
+
const zh = locale === 'zh';
|
|
106
|
+
const L = [];
|
|
107
|
+
L.push(zh ? '## 🔍 项目实测风险' : '## 🔍 Project Scan Findings');
|
|
108
|
+
L.push('');
|
|
109
|
+
L.push(zh
|
|
110
|
+
? `> 已扫描 ${scan.filesScanned} 个文件${scan.truncated ? '(已达上限,部分未扫)' : ''}`
|
|
111
|
+
: `> Scanned ${scan.filesScanned} files${scan.truncated ? ' (limit reached, partial)' : ''}`);
|
|
112
|
+
L.push('');
|
|
113
|
+
if (scan.findings.length === 0) {
|
|
114
|
+
L.push(zh ? '🟢 未在项目文件中发现硬编码密钥、个人信息暴露或境外端点。' : '🟢 No hardcoded secrets, PII exposure, or overseas endpoints found in project files.');
|
|
115
|
+
L.push('');
|
|
116
|
+
return L.join('\n');
|
|
117
|
+
}
|
|
118
|
+
// 概览计数
|
|
119
|
+
const counts = KIND_ORDER
|
|
120
|
+
.filter(k => scan.counts[k] > 0)
|
|
121
|
+
.map(k => `${KIND_LABEL[k].icon} ${zh ? KIND_LABEL[k].zh : KIND_LABEL[k].en}: ${scan.counts[k]}`)
|
|
122
|
+
.join(' | ');
|
|
123
|
+
L.push(`**${counts}**`);
|
|
124
|
+
L.push('');
|
|
125
|
+
// 按类型分组列出(每类最多 15 条,避免报告过长)
|
|
126
|
+
for (const kind of KIND_ORDER) {
|
|
127
|
+
const items = scan.findings.filter(f => f.kind === kind);
|
|
128
|
+
if (items.length === 0)
|
|
129
|
+
continue;
|
|
130
|
+
const label = KIND_LABEL[kind];
|
|
131
|
+
L.push(`### ${label.icon} ${zh ? label.zh : label.en} (${items.length})`);
|
|
132
|
+
L.push('');
|
|
133
|
+
for (const f of items.slice(0, 15)) {
|
|
134
|
+
const loc = f.line ? `${f.file}:${f.line}` : f.file;
|
|
135
|
+
L.push(`- \`${loc}\` — ${f.detail}`);
|
|
136
|
+
}
|
|
137
|
+
if (items.length > 15) {
|
|
138
|
+
L.push(zh ? `- …其余 ${items.length - 15} 项` : `- …and ${items.length - 15} more`);
|
|
139
|
+
}
|
|
140
|
+
L.push('');
|
|
141
|
+
}
|
|
142
|
+
return L.join('\n');
|
|
143
|
+
}
|
|
144
|
+
function scoreBar(score) {
|
|
145
|
+
const filled = Math.round(score / 5);
|
|
146
|
+
return '█'.repeat(filled) + '░'.repeat(20 - filled);
|
|
147
|
+
}
|
|
148
|
+
function gradeBadge(grade) {
|
|
149
|
+
const map = {
|
|
150
|
+
A: '🏆 优秀', B: '✅ 良好', C: '⚠️ 及格', D: '❌ 不及格',
|
|
151
|
+
};
|
|
152
|
+
return map[grade] || grade;
|
|
153
|
+
}
|
|
154
|
+
function severityRank(r) {
|
|
155
|
+
return { critical: 4, high: 3, medium: 2, low: 1 }[r.control.severity];
|
|
156
|
+
}
|
|
157
|
+
function regName(reg, zh) {
|
|
158
|
+
return zh ? REGULATION_NAMES[reg].zh : REGULATION_NAMES[reg].en;
|
|
159
|
+
}
|
|
160
|
+
function groupByRegulation(results) {
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const r of results) {
|
|
163
|
+
;
|
|
164
|
+
(out[r.control.regulation] ||= []).push(r);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|