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.
- 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 +215 -0
- package/dist/mcp-server.js +36 -0
- package/dist/rules/domestic-alternatives.d.ts +27 -0
- package/dist/rules/domestic-alternatives.js +62 -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 +242 -0
- package/src/mcp-server.ts +37 -0
- package/src/rules/domestic-alternatives.ts +90 -0
- package/src/rules/overseas-llm.ts +174 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli.ts — ShellWard CLI 入口(零安装合规体检)
|
|
3
|
+
//
|
|
4
|
+
// npx shellward → 扫描当前项目,输出合规体检评分卡
|
|
5
|
+
// npx shellward scan [dir] → 同上,可指定目录
|
|
6
|
+
// npx shellward scan --json→ 输出 JSON(CI 用)
|
|
7
|
+
// npx shellward mcp → 启动 MCP 服务器(stdio,向后兼容)
|
|
8
|
+
// npx shellward --help
|
|
9
|
+
//
|
|
10
|
+
// 设计目标:30 秒、零配置、出一张可截图的「你的项目」合规风险报告。
|
|
11
|
+
|
|
12
|
+
import { resolve } from 'path'
|
|
13
|
+
import { writeFileSync } from 'fs'
|
|
14
|
+
import { ShellWard } from './core/engine.js'
|
|
15
|
+
import { runProjectComplianceAudit } from './compliance/audit.js'
|
|
16
|
+
import { renderComplianceReport, renderProjectFindings } from './compliance/report.js'
|
|
17
|
+
import { resolveLocale } from './types.js'
|
|
18
|
+
|
|
19
|
+
const argv = process.argv.slice(2)
|
|
20
|
+
const wantsHelp = argv.includes('--help') || argv.includes('-h') || argv[0] === 'help'
|
|
21
|
+
const cmd = argv[0] && !argv[0].startsWith('-') ? argv[0] : 'scan'
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
if (wantsHelp) {
|
|
25
|
+
printHelp()
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (cmd === 'mcp') {
|
|
30
|
+
// 转发到 MCP 服务器(import 即启动 stdio 循环)
|
|
31
|
+
await import('./mcp-server.js')
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (cmd === 'scan') {
|
|
36
|
+
runScan(argv.slice(1))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.error(`未知命令: ${cmd}\n`)
|
|
41
|
+
printHelp()
|
|
42
|
+
process.exit(2)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runScan(args: string[]) {
|
|
46
|
+
const json = args.includes('--json')
|
|
47
|
+
const ci = args.includes('--ci')
|
|
48
|
+
const outPath = flagValue(args, '--out')
|
|
49
|
+
const dirArg = args.find(a => !a.startsWith('-'))
|
|
50
|
+
const root = resolve(dirArg || process.cwd())
|
|
51
|
+
|
|
52
|
+
// 用环境变量解析 locale;layers/mode 用默认(代表「采用 ShellWard 默认部署」的合规覆盖)
|
|
53
|
+
const guard = new ShellWard({
|
|
54
|
+
locale: (process.env.SHELLWARD_LOCALE as any) || 'auto',
|
|
55
|
+
mode: (process.env.SHELLWARD_MODE as any) || 'enforce',
|
|
56
|
+
autoCheckOnStartup: false,
|
|
57
|
+
})
|
|
58
|
+
const locale = resolveLocale(guard.config)
|
|
59
|
+
const zh = locale === 'zh'
|
|
60
|
+
|
|
61
|
+
const { report, scan } = runProjectComplianceAudit(guard.config, root)
|
|
62
|
+
|
|
63
|
+
if (json) {
|
|
64
|
+
process.stdout.write(JSON.stringify({
|
|
65
|
+
root,
|
|
66
|
+
score: report.score,
|
|
67
|
+
grade: report.grade,
|
|
68
|
+
summary: { passed: report.passed, warned: report.warned, failed: report.failed, manual: report.manual },
|
|
69
|
+
projectScan: {
|
|
70
|
+
filesScanned: scan.filesScanned,
|
|
71
|
+
truncated: scan.truncated,
|
|
72
|
+
counts: scan.counts,
|
|
73
|
+
findings: scan.findings,
|
|
74
|
+
},
|
|
75
|
+
controls: report.results.map(r => ({
|
|
76
|
+
id: r.control.id, regulation: r.control.regulation, status: r.status,
|
|
77
|
+
})),
|
|
78
|
+
}, null, 2) + '\n')
|
|
79
|
+
} else {
|
|
80
|
+
// 头条:项目实测风险(关于「你的项目」)+ 合规映射评分卡
|
|
81
|
+
const body = [
|
|
82
|
+
renderProjectFindings(scan, locale),
|
|
83
|
+
renderComplianceReport(report, locale),
|
|
84
|
+
].join('\n')
|
|
85
|
+
|
|
86
|
+
if (outPath) {
|
|
87
|
+
const doc = `<!-- 扫描目录: ${root} -->\n\n` + body + '\n'
|
|
88
|
+
writeFileSync(resolve(outPath), doc, 'utf-8')
|
|
89
|
+
process.stdout.write(zh
|
|
90
|
+
? `✅ 合规报告已导出: ${resolve(outPath)}\n 得分 ${report.score}/100 [${report.grade}],可存档用于备案/审计。\n`
|
|
91
|
+
: `✅ Compliance report exported: ${resolve(outPath)}\n Score ${report.score}/100 [${report.grade}].\n`)
|
|
92
|
+
} else {
|
|
93
|
+
const out = [
|
|
94
|
+
zh ? `\n扫描目录: ${root}\n` : `\nScanned: ${root}\n`,
|
|
95
|
+
body,
|
|
96
|
+
'',
|
|
97
|
+
zh
|
|
98
|
+
? '💡 这是只读扫描,未上传任何数据。要在运行时自动拦截风险,把 ShellWard 作为 MCP/插件接入你的 AI Agent。'
|
|
99
|
+
: '💡 Read-only scan, nothing uploaded. To block these risks at runtime, integrate ShellWard as an MCP server/plugin in your AI agent.',
|
|
100
|
+
]
|
|
101
|
+
process.stdout.write(out.join('\n') + '\n')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// CI 模式:有 critical 项目发现则非零退出
|
|
106
|
+
if (ci) {
|
|
107
|
+
const criticals = scan.findings.filter(f => f.severity === 'critical').length
|
|
108
|
+
if (criticals > 0) process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 取 `--flag value` 或 `--flag=value` 的值 */
|
|
113
|
+
function flagValue(args: string[], flag: string): string | undefined {
|
|
114
|
+
const i = args.indexOf(flag)
|
|
115
|
+
if (i >= 0 && args[i + 1] && !args[i + 1].startsWith('-')) return args[i + 1]
|
|
116
|
+
const eq = args.find(a => a.startsWith(flag + '='))
|
|
117
|
+
return eq ? eq.slice(flag.length + 1) : undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function printHelp() {
|
|
121
|
+
const lang = (process.env.SHELLWARD_LOCALE === 'en') ? 'en' : 'zh'
|
|
122
|
+
if (lang === 'en') {
|
|
123
|
+
console.log(`ShellWard — AI compliance gateway
|
|
124
|
+
|
|
125
|
+
Usage:
|
|
126
|
+
shellward [scan] [dir] Scan a project for compliance risks (default)
|
|
127
|
+
shellward scan --json Output JSON (for CI)
|
|
128
|
+
shellward scan --ci Exit non-zero if critical findings
|
|
129
|
+
shellward scan --out f Export the full report to a Markdown file
|
|
130
|
+
shellward mcp Start MCP server (stdio)
|
|
131
|
+
shellward --help
|
|
132
|
+
|
|
133
|
+
Detects: overseas LLM endpoints (data-export risk), hardcoded secrets,
|
|
134
|
+
PII in files, .env permissions. Maps to CSL / PIPL / MLPS / cross-border / labeling.`)
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`ShellWard — AI 合规网关
|
|
137
|
+
|
|
138
|
+
用法:
|
|
139
|
+
shellward [scan] [目录] 扫描项目的合规风险(默认命令)
|
|
140
|
+
shellward scan --json 输出 JSON(CI 用)
|
|
141
|
+
shellward scan --ci 有 critical 发现时非零退出
|
|
142
|
+
shellward scan --out 文件 导出完整报告为 Markdown(合规存档)
|
|
143
|
+
shellward mcp 启动 MCP 服务器(stdio)
|
|
144
|
+
shellward --help
|
|
145
|
+
|
|
146
|
+
检测: 境外大模型端点(数据出境)、硬编码密钥、文件中的个人信息、.env 权限。
|
|
147
|
+
映射到 网安法 / PIPL / 等保2.0 / 数据出境 / AI标识。`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main().catch(err => {
|
|
152
|
+
console.error(`[ShellWard] ${err?.message || err}`)
|
|
153
|
+
process.exit(1)
|
|
154
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/commands/compliance.ts — /compliance 命令:一键合规体检报告
|
|
2
|
+
//
|
|
3
|
+
// 月1 获客钩子的命令形态。扫一遍配置 + 环境 + 审计日志 + 出境端点,
|
|
4
|
+
// 输出网安法/PIPL/等保/数据出境/AI标识 的红黄绿合规评分卡。
|
|
5
|
+
|
|
6
|
+
import type { ShellWardConfig } from '../types.js'
|
|
7
|
+
import { resolveLocale } from '../types.js'
|
|
8
|
+
import { runComplianceAudit } from '../compliance/audit.js'
|
|
9
|
+
import { renderComplianceReport } from '../compliance/report.js'
|
|
10
|
+
|
|
11
|
+
export function registerComplianceCommand(api: any, config: ShellWardConfig) {
|
|
12
|
+
const locale = resolveLocale(config)
|
|
13
|
+
|
|
14
|
+
api.registerCommand({
|
|
15
|
+
name: 'compliance',
|
|
16
|
+
description: locale === 'zh'
|
|
17
|
+
? '📋 AI 应用合规体检(网安法/PIPL/等保/数据出境/AI标识)'
|
|
18
|
+
: '📋 AI compliance health check (CSL/PIPL/MLPS/Cross-border/Labeling)',
|
|
19
|
+
acceptsArgs: false,
|
|
20
|
+
handler: () => {
|
|
21
|
+
const report = runComplianceAudit(config)
|
|
22
|
+
return { text: renderComplianceReport(report, locale) }
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
}
|
package/src/commands/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { registerScanPluginsCommand } from './scan-plugins.js'
|
|
|
9
9
|
import { registerScanMcpCommand } from './scan-mcp.js'
|
|
10
10
|
import { registerCheckUpdatesCommand } from './check-updates.js'
|
|
11
11
|
import { registerUpgradeOpenClawCommand } from './upgrade-openclaw.js'
|
|
12
|
+
import { registerComplianceCommand } from './compliance.js'
|
|
12
13
|
|
|
13
14
|
/** @returns number of registered commands (for the startup log). */
|
|
14
15
|
export function registerAllCommands(api: any, config: ShellWardConfig): number {
|
|
@@ -22,6 +23,7 @@ export function registerAllCommands(api: any, config: ShellWardConfig): number {
|
|
|
22
23
|
registerScanMcpCommand(api, config)
|
|
23
24
|
registerCheckUpdatesCommand(api, config)
|
|
24
25
|
registerUpgradeOpenClawCommand(api, config)
|
|
26
|
+
registerComplianceCommand(api, config)
|
|
25
27
|
|
|
26
28
|
// Register /cg shortcut with help
|
|
27
29
|
api.registerCommand({
|
|
@@ -35,6 +37,7 @@ export function registerAllCommands(api: any, config: ShellWardConfig): number {
|
|
|
35
37
|
|
|
36
38
|
| 命令 | 说明 |
|
|
37
39
|
|------|------|
|
|
40
|
+
| \`/compliance\` | 🆕 AI 合规体检(网安法/PIPL/等保/数据出境/AI标识 红黄绿评分卡) |
|
|
38
41
|
| \`/security\` | 安全状态总览(防御层、审计统计、系统检查) |
|
|
39
42
|
| \`/audit [数量] [过滤]\` | 查看审计日志 (过滤: block/audit/critical/high) |
|
|
40
43
|
| \`/harden\` | 安全扫描 · \`/harden fix\` 自动修复权限 |
|
|
@@ -50,6 +53,7 @@ L5 安全门 · L6 回复审计 · L7 数据流监控 · L8 会话安全`
|
|
|
50
53
|
|
|
51
54
|
| Command | Description |
|
|
52
55
|
|---------|-------------|
|
|
56
|
+
| \`/compliance\` | 🆕 AI compliance check (CSL/PIPL/MLPS/cross-border/labeling scorecard) |
|
|
53
57
|
| \`/security\` | Security status overview (layers, audit stats, system checks) |
|
|
54
58
|
| \`/audit [count] [filter]\` | View audit log (filter: block/audit/critical/high) |
|
|
55
59
|
| \`/harden\` | Security scan · \`/harden fix\` to auto-fix permissions |
|
|
@@ -64,6 +68,6 @@ L5 Security Gate · L6 Outbound Guard · L7 Data Flow Guard · L8 Session Guard`
|
|
|
64
68
|
}),
|
|
65
69
|
})
|
|
66
70
|
|
|
67
|
-
//
|
|
68
|
-
return
|
|
71
|
+
// 8 individual commands + /cg help
|
|
72
|
+
return 9
|
|
69
73
|
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// src/compliance/audit.ts — 合规体检引擎
|
|
2
|
+
//
|
|
3
|
+
// 跑遍 COMPLIANCE_CONTROLS,对每个控制项给出 pass / warn / fail / manual,
|
|
4
|
+
// 汇总成红黄绿评分卡。这是「一键合规体检报告」(月1 获客钩子) 的核心。
|
|
5
|
+
//
|
|
6
|
+
// 设计为可注入 (EnvFacts):测试可直接喂事实,运行时则从真实环境采集。
|
|
7
|
+
|
|
8
|
+
import { readFileSync, statSync } from 'fs'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
import { getHomeDir } from '../utils.js'
|
|
11
|
+
import { detectOverseasLLM } from '../rules/overseas-llm.js'
|
|
12
|
+
import type { OverseasMatch } from '../rules/overseas-llm.js'
|
|
13
|
+
import { scanProject } from './project-scan.js'
|
|
14
|
+
import type { ProjectScanResult } from './project-scan.js'
|
|
15
|
+
import { COMPLIANCE_CONTROLS } from './regulations.js'
|
|
16
|
+
import type { ComplianceControl, Regulation, Severity } from './regulations.js'
|
|
17
|
+
import type { ShellWardConfig } from '../types.js'
|
|
18
|
+
|
|
19
|
+
const LOG_FILE = join(getHomeDir(), '.openclaw', 'shellward', 'audit.jsonl')
|
|
20
|
+
const SIX_MONTHS_MS = 182 * 24 * 60 * 60 * 1000
|
|
21
|
+
|
|
22
|
+
export type ControlStatus = 'pass' | 'warn' | 'fail' | 'manual'
|
|
23
|
+
|
|
24
|
+
export interface ControlResult {
|
|
25
|
+
control: ComplianceControl
|
|
26
|
+
status: ControlStatus
|
|
27
|
+
detail_zh: string
|
|
28
|
+
detail_en: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AuditLogFacts {
|
|
32
|
+
exists: boolean
|
|
33
|
+
entryCount: number
|
|
34
|
+
/** 最早一条记录时间戳 (ISO),用于判断是否覆盖 6 个月留存 */
|
|
35
|
+
oldestTs?: string
|
|
36
|
+
newestTs?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface EnvFacts {
|
|
40
|
+
isRoot: boolean
|
|
41
|
+
auditLog: AuditLogFacts
|
|
42
|
+
/** 从环境/配置中探测到的境外大模型端点 */
|
|
43
|
+
overseas: OverseasMatch[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ComplianceReport {
|
|
47
|
+
/** 0-100 合规得分 */
|
|
48
|
+
score: number
|
|
49
|
+
/** 总评级 */
|
|
50
|
+
grade: 'A' | 'B' | 'C' | 'D'
|
|
51
|
+
passed: number
|
|
52
|
+
warned: number
|
|
53
|
+
failed: number
|
|
54
|
+
manual: number
|
|
55
|
+
total: number
|
|
56
|
+
results: ControlResult[]
|
|
57
|
+
generatedAt: string
|
|
58
|
+
/** 项目实测风险造成的扣分(仅项目体检路径);0 表示纯控制项评分 */
|
|
59
|
+
projectPenalty?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 层能力映射:控制项 id → 必须启用的层(全部启用才 pass,部分启用 warn,全关 fail) */
|
|
63
|
+
const CAPABILITY_LAYERS: Record<string, (keyof ShellWardConfig['layers'])[]> = {
|
|
64
|
+
'csl-content-block': ['outputScanner', 'outboundGuard'],
|
|
65
|
+
'csl-intrusion': ['inputAuditor', 'toolBlocker'],
|
|
66
|
+
'pipl-spi-detect': ['outputScanner'],
|
|
67
|
+
'pipl-minimize': ['dataFlowGuard'],
|
|
68
|
+
'pipl-auto-decision': ['securityGate'],
|
|
69
|
+
'mlps-access-control': ['securityGate'],
|
|
70
|
+
'cbdt-redact-before-export': ['dataFlowGuard'],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 采集真实环境事实(运行时调用;测试可绕过直接注入 EnvFacts) */
|
|
74
|
+
export function gatherEnvFacts(): EnvFacts {
|
|
75
|
+
// 1. root 检测
|
|
76
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0
|
|
77
|
+
|
|
78
|
+
// 2. 审计日志事实
|
|
79
|
+
const auditLog = readAuditLogFacts()
|
|
80
|
+
|
|
81
|
+
// 3. 出境端点探测:扫描常见环境变量中的 base_url / 端点
|
|
82
|
+
const overseas: OverseasMatch[] = []
|
|
83
|
+
const seen = new Set<string>()
|
|
84
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
85
|
+
if (!v) continue
|
|
86
|
+
if (!/(_BASE_URL|_API_BASE|_ENDPOINT|_URL|OPENAI|ANTHROPIC|GEMINI|LLM)/i.test(k)) continue
|
|
87
|
+
const m = detectOverseasLLM(v)
|
|
88
|
+
if (m.isOverseas && m.endpointId && !seen.has(m.endpointId)) {
|
|
89
|
+
seen.add(m.endpointId)
|
|
90
|
+
overseas.push(m)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { isRoot, auditLog, overseas }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readAuditLogFacts(): AuditLogFacts {
|
|
98
|
+
try {
|
|
99
|
+
statSync(LOG_FILE)
|
|
100
|
+
const content = readFileSync(LOG_FILE, 'utf-8')
|
|
101
|
+
const lines = content.trim().split('\n').filter(Boolean)
|
|
102
|
+
if (lines.length === 0) return { exists: true, entryCount: 0 }
|
|
103
|
+
const firstTs = extractTs(lines[0])
|
|
104
|
+
const lastTs = extractTs(lines[lines.length - 1])
|
|
105
|
+
return { exists: true, entryCount: lines.length, oldestTs: firstTs, newestTs: lastTs }
|
|
106
|
+
} catch {
|
|
107
|
+
return { exists: false, entryCount: 0 }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractTs(line: string): string | undefined {
|
|
112
|
+
const m = line.match(/"ts":"([^"]+)"/)
|
|
113
|
+
return m?.[1]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 运行合规体检。
|
|
118
|
+
* @param config ShellWard 当前配置
|
|
119
|
+
* @param facts 环境事实;不传则从真实环境采集
|
|
120
|
+
*/
|
|
121
|
+
export function runComplianceAudit(config: ShellWardConfig, facts?: EnvFacts): ComplianceReport {
|
|
122
|
+
const env = facts ?? gatherEnvFacts()
|
|
123
|
+
const results: ControlResult[] = COMPLIANCE_CONTROLS.map(c => checkControl(c, config, env))
|
|
124
|
+
|
|
125
|
+
let passed = 0, warned = 0, failed = 0, manual = 0
|
|
126
|
+
for (const r of results) {
|
|
127
|
+
if (r.status === 'pass') passed++
|
|
128
|
+
else if (r.status === 'warn') warned++
|
|
129
|
+
else if (r.status === 'fail') failed++
|
|
130
|
+
else manual++
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const score = computeScore(results)
|
|
134
|
+
return {
|
|
135
|
+
score,
|
|
136
|
+
grade: gradeOf(score),
|
|
137
|
+
passed, warned, failed, manual,
|
|
138
|
+
total: results.length,
|
|
139
|
+
results,
|
|
140
|
+
generatedAt: new Date().toISOString(),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ProjectComplianceResult {
|
|
145
|
+
report: ComplianceReport
|
|
146
|
+
scan: ProjectScanResult
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 面向真实项目的体检:扫描项目目录的真实风险,并入评分,再跑控制项体检。
|
|
151
|
+
* 这是 CLI (`shellward scan`) 的入口 —— 报告关于「用户项目」而非「ShellWard 开关」。
|
|
152
|
+
*/
|
|
153
|
+
export function runProjectComplianceAudit(config: ShellWardConfig, root: string): ProjectComplianceResult {
|
|
154
|
+
const scan = scanProject(root)
|
|
155
|
+
const env = gatherEnvFacts()
|
|
156
|
+
|
|
157
|
+
// 把文件中实测到的境外端点/依赖并入 facts(按 endpointId 或 provider 去重),
|
|
158
|
+
// 使数据出境项基于真实证据(含 SDK 依赖通道)
|
|
159
|
+
const seen = new Set(env.overseas.map(o => o.endpointId || o.provider_en))
|
|
160
|
+
for (const f of scan.findings) {
|
|
161
|
+
if (f.kind !== 'overseas') continue
|
|
162
|
+
const key = f.endpointId || f.provider_en || ''
|
|
163
|
+
if (!key || seen.has(key)) continue
|
|
164
|
+
seen.add(key)
|
|
165
|
+
env.overseas.push({
|
|
166
|
+
isOverseas: true,
|
|
167
|
+
endpointId: f.endpointId,
|
|
168
|
+
provider_zh: f.provider_zh,
|
|
169
|
+
provider_en: f.provider_en,
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const report = runComplianceAudit(config, env)
|
|
174
|
+
|
|
175
|
+
// 发现驱动评分:项目实测风险按严重度扣分(封顶 40),使分数反映"你的真实风险"
|
|
176
|
+
const penalty = computeProjectPenalty(scan)
|
|
177
|
+
if (penalty > 0) {
|
|
178
|
+
report.score = Math.max(0, report.score - penalty)
|
|
179
|
+
report.grade = gradeOf(report.score)
|
|
180
|
+
report.projectPenalty = penalty
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { report, scan }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const FINDING_PENALTY = { critical: 8, high: 4, medium: 1 } as const
|
|
187
|
+
const MAX_PROJECT_PENALTY = 40
|
|
188
|
+
|
|
189
|
+
function computeProjectPenalty(scan: ProjectScanResult): number {
|
|
190
|
+
let p = 0
|
|
191
|
+
for (const f of scan.findings) p += FINDING_PENALTY[f.severity]
|
|
192
|
+
return Math.min(MAX_PROJECT_PENALTY, p)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function checkControl(c: ComplianceControl, config: ShellWardConfig, env: EnvFacts): ControlResult {
|
|
196
|
+
switch (c.method) {
|
|
197
|
+
case 'capability': return checkCapability(c, config)
|
|
198
|
+
case 'config': return checkConfig(c, config)
|
|
199
|
+
case 'audit': return checkAudit(c, env)
|
|
200
|
+
case 'env': return checkEnv(c, env)
|
|
201
|
+
case 'manual': return mk(c, 'manual',
|
|
202
|
+
'需人工确认 / 路线图功能:' + c.remediation_zh,
|
|
203
|
+
'Manual / roadmap: ' + c.remediation_en)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function checkCapability(c: ComplianceControl, config: ShellWardConfig): ControlResult {
|
|
208
|
+
const required = CAPABILITY_LAYERS[c.id]
|
|
209
|
+
if (!required) {
|
|
210
|
+
// 未显式映射的能力项:以 enforce 模式作为兜底信号
|
|
211
|
+
return config.mode === 'enforce'
|
|
212
|
+
? mk(c, 'pass', '能力已启用 (enforce 模式)', 'Capability active (enforce mode)')
|
|
213
|
+
: mk(c, 'warn', 'audit 模式仅记录不拦截,建议切换 enforce', 'Audit mode logs only; switch to enforce')
|
|
214
|
+
}
|
|
215
|
+
const on = required.filter(l => config.layers[l])
|
|
216
|
+
if (on.length === required.length) {
|
|
217
|
+
const tail = config.mode === 'enforce' ? '' : '(注意:audit 模式仅记录不拦截)'
|
|
218
|
+
return mk(c, config.mode === 'enforce' ? 'pass' : 'warn',
|
|
219
|
+
`已启用: ${required.join(', ')}${tail}`,
|
|
220
|
+
`Enabled: ${required.join(', ')}${config.mode === 'enforce' ? '' : ' (audit mode: log-only)'}`)
|
|
221
|
+
}
|
|
222
|
+
if (on.length > 0) {
|
|
223
|
+
return mk(c, 'warn',
|
|
224
|
+
`部分启用: ${on.join(', ')};缺少: ${required.filter(l => !on.includes(l)).join(', ')}`,
|
|
225
|
+
`Partially enabled; missing: ${required.filter(l => !on.includes(l)).join(', ')}`)
|
|
226
|
+
}
|
|
227
|
+
return mk(c, 'fail', `未启用: ${required.join(', ')}`, `Not enabled: ${required.join(', ')}`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function checkConfig(c: ComplianceControl, config: ShellWardConfig): ControlResult {
|
|
231
|
+
return config.mode === 'enforce'
|
|
232
|
+
? mk(c, 'pass', 'enforce 模式', 'enforce mode')
|
|
233
|
+
: mk(c, 'warn', 'audit 模式仅记录', 'audit mode logs only')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function checkAudit(c: ComplianceControl, env: EnvFacts): ControlResult {
|
|
237
|
+
const a = env.auditLog
|
|
238
|
+
if (!a.exists || a.entryCount === 0) {
|
|
239
|
+
return mk(c, 'fail',
|
|
240
|
+
'未发现审计日志或日志为空 — 无法满足留存与举证要求',
|
|
241
|
+
'No audit log found or empty — retention/evidence requirement unmet')
|
|
242
|
+
}
|
|
243
|
+
// 判断留存跨度是否覆盖 6 个月
|
|
244
|
+
if (a.oldestTs) {
|
|
245
|
+
const span = Date.now() - new Date(a.oldestTs).getTime()
|
|
246
|
+
if (span >= SIX_MONTHS_MS) {
|
|
247
|
+
return mk(c, 'pass',
|
|
248
|
+
`审计日志 ${a.entryCount} 条,最早 ${a.oldestTs.slice(0, 10)},已覆盖 ≥6 个月`,
|
|
249
|
+
`${a.entryCount} entries since ${a.oldestTs.slice(0, 10)}, ≥6 months covered`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return mk(c, 'warn',
|
|
253
|
+
`审计日志已启用 (${a.entryCount} 条),但留存尚未满 6 个月 — 需持续运行积累`,
|
|
254
|
+
`Audit log active (${a.entryCount} entries) but <6 months retained — keep running`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function checkEnv(c: ComplianceControl, env: EnvFacts): ControlResult {
|
|
258
|
+
if (c.id === 'mlps-not-root') {
|
|
259
|
+
return env.isRoot
|
|
260
|
+
? mk(c, 'fail', '正在以 root 运行 — 违反最小权限原则', 'Running as root — violates least privilege')
|
|
261
|
+
: mk(c, 'pass', '非 root 运行', 'Not running as root')
|
|
262
|
+
}
|
|
263
|
+
if (c.id === 'cbdt-overseas-llm') {
|
|
264
|
+
if (env.overseas.length > 0) {
|
|
265
|
+
const names = env.overseas.map(o => o.provider_zh).join(', ')
|
|
266
|
+
const namesEn = env.overseas.map(o => o.provider_en).join(', ')
|
|
267
|
+
return mk(c, 'fail',
|
|
268
|
+
`检测到境外大模型端点配置: ${names} — 若向其发送个人信息/重要数据即构成数据出境,须走合规路径`,
|
|
269
|
+
`Overseas LLM endpoint(s) configured: ${namesEn} — sending PI/important data = cross-border export`)
|
|
270
|
+
}
|
|
271
|
+
return mk(c, 'pass', '未检测到境外大模型端点配置', 'No overseas LLM endpoint detected')
|
|
272
|
+
}
|
|
273
|
+
return mk(c, 'manual', '需人工确认', 'Manual check required')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function mk(control: ComplianceControl, status: ControlStatus, detail_zh: string, detail_en: string): ControlResult {
|
|
277
|
+
return { control, status, detail_zh, detail_en }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ===== 评分 =====
|
|
281
|
+
|
|
282
|
+
const SEVERITY_WEIGHT: Record<Severity, number> = {
|
|
283
|
+
critical: 4, high: 3, medium: 2, low: 1,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 加权得分:manual 项不计入分母(不惩罚路线图/人工项)。
|
|
288
|
+
* pass=满分, warn=半分, fail=0。
|
|
289
|
+
*/
|
|
290
|
+
function computeScore(results: ControlResult[]): number {
|
|
291
|
+
let earned = 0, possible = 0
|
|
292
|
+
for (const r of results) {
|
|
293
|
+
if (r.status === 'manual') continue
|
|
294
|
+
const w = SEVERITY_WEIGHT[r.control.severity]
|
|
295
|
+
possible += w
|
|
296
|
+
if (r.status === 'pass') earned += w
|
|
297
|
+
else if (r.status === 'warn') earned += w * 0.5
|
|
298
|
+
}
|
|
299
|
+
if (possible === 0) return 0
|
|
300
|
+
return Math.round((earned / possible) * 100)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function gradeOf(score: number): 'A' | 'B' | 'C' | 'D' {
|
|
304
|
+
if (score >= 90) return 'A'
|
|
305
|
+
if (score >= 75) return 'B'
|
|
306
|
+
if (score >= 60) return 'C'
|
|
307
|
+
return 'D'
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export type { Regulation }
|