shellward 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -230
- package/openclaw.plugin.json +7 -2
- package/package.json +25 -8
- package/src/audit-log.ts +12 -2
- package/src/auto-check.ts +177 -0
- package/src/commands/audit.ts +7 -4
- package/src/commands/check-updates.ts +72 -47
- package/src/commands/harden.ts +39 -1
- package/src/commands/index.ts +8 -4
- package/src/commands/scan-plugins.ts +21 -5
- package/src/commands/security.ts +8 -4
- package/src/commands/upgrade-openclaw.ts +58 -0
- package/src/core/engine.ts +667 -0
- package/src/index.ts +98 -69
- package/src/layers/data-flow-guard.ts +14 -145
- package/src/layers/input-auditor.ts +18 -157
- package/src/layers/outbound-guard.ts +11 -54
- package/src/layers/output-scanner.ts +6 -79
- package/src/layers/prompt-guard.ts +6 -59
- package/src/layers/security-gate.ts +11 -86
- package/src/layers/session-guard.ts +8 -23
- package/src/layers/tool-blocker.ts +20 -167
- package/src/rules/dangerous-commands.ts +13 -1
- package/src/rules/injection-en.ts +16 -0
- package/src/rules/injection-zh.ts +39 -3
- package/src/rules/sensitive-patterns.ts +2 -2
- package/src/types.ts +7 -4
- package/src/update-check.ts +186 -0
- package/src/utils.ts +10 -0
- package/vuln-db.json +137 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/auto-check.ts — 启动时自动安全检查,减少人为操作
|
|
2
|
+
// 异步执行,不阻塞启动;发现问题时通过 logger 告警
|
|
3
|
+
|
|
4
|
+
import { execSync } from 'child_process'
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { getHomeDir } from './utils'
|
|
8
|
+
import { fetchVulnDB, compareVersions } from './update-check'
|
|
9
|
+
|
|
10
|
+
const OPENCLAW_DIR = join(getHomeDir(), '.openclaw')
|
|
11
|
+
|
|
12
|
+
export interface AutoCheckResult {
|
|
13
|
+
openclawVulns: { id: string; severity: string; description: string }[]
|
|
14
|
+
pluginRisks: { plugin: string; risk: string }[]
|
|
15
|
+
mcpRisks: { config: string; risk: string }[]
|
|
16
|
+
rootWarning: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SUSPICIOUS_PATTERNS = [
|
|
20
|
+
{ pattern: /eval\s*\(/, name: 'eval()' },
|
|
21
|
+
{ pattern: /\/dev\/tcp|nc\s+-e|ncat/, name: 'reverse shell' },
|
|
22
|
+
{ pattern: /webhook|exfil|callback.*http/i, name: 'data exfil' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 获取 OpenClaw 版本
|
|
27
|
+
*/
|
|
28
|
+
function getOpenClawVersion(): string {
|
|
29
|
+
try {
|
|
30
|
+
const out = execSync('openclaw --version 2>&1', { timeout: 5000 }).toString().trim()
|
|
31
|
+
const match = out.match(/(\d{4}\.\d+\.\d+|\d+\.\d+\.\d+)/)
|
|
32
|
+
return match ? match[1] : 'unknown'
|
|
33
|
+
} catch {
|
|
34
|
+
return 'unknown'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 检查 OpenClaw 是否受已知漏洞影响
|
|
40
|
+
*/
|
|
41
|
+
async function checkOpenClawVulns(version: string): Promise<{ id: string; severity: string; description: string }[]> {
|
|
42
|
+
const vulns: { id: string; severity: string; description: string }[] = []
|
|
43
|
+
try {
|
|
44
|
+
const { vulns: db } = await fetchVulnDB()
|
|
45
|
+
const list = db.length > 0 ? db : [
|
|
46
|
+
{ affectedBelow: '1.0.111', severity: 'HIGH' as const, id: 'CVE-2025-59536', description_zh: 'RCE via Hooks/MCP', description_en: 'RCE via Hooks/MCP' },
|
|
47
|
+
{ affectedBelow: '2.0.65', severity: 'MEDIUM' as const, id: 'CVE-2026-21852', description_zh: 'API Key exfil', description_en: 'API Key exfil' },
|
|
48
|
+
]
|
|
49
|
+
for (const v of list) {
|
|
50
|
+
if (version !== 'unknown' && compareVersions(version, v.affectedBelow) < 0) {
|
|
51
|
+
vulns.push({
|
|
52
|
+
id: v.id,
|
|
53
|
+
severity: v.severity || 'MEDIUM',
|
|
54
|
+
description: (v as any).description_zh || (v as any).description_en || v.id,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
return vulns
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 快速扫描插件中的高风险模式
|
|
64
|
+
*/
|
|
65
|
+
function scanPluginsQuick(): { plugin: string; risk: string }[] {
|
|
66
|
+
const risks: { plugin: string; risk: string }[] = []
|
|
67
|
+
const dirs = [
|
|
68
|
+
join(OPENCLAW_DIR, 'extensions'),
|
|
69
|
+
join(OPENCLAW_DIR, 'plugins'),
|
|
70
|
+
]
|
|
71
|
+
for (const dir of dirs) {
|
|
72
|
+
if (!existsSync(dir)) continue
|
|
73
|
+
try {
|
|
74
|
+
for (const name of readdirSync(dir)) {
|
|
75
|
+
const p = join(dir, name)
|
|
76
|
+
if (name.startsWith('.')) continue
|
|
77
|
+
try {
|
|
78
|
+
const files = readdirSync(p)
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
if (!/\.(ts|js)$/.test(f)) continue
|
|
81
|
+
const content = readFileSync(join(p, f), 'utf-8').slice(0, 50000)
|
|
82
|
+
for (const { pattern, name: riskName } of SUSPICIOUS_PATTERNS) {
|
|
83
|
+
if (pattern.test(content)) {
|
|
84
|
+
risks.push({ plugin: name, risk: riskName })
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch { /* skip */ }
|
|
90
|
+
}
|
|
91
|
+
} catch { /* skip */ }
|
|
92
|
+
}
|
|
93
|
+
return risks
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 扫描 MCP 配置中的可疑项
|
|
98
|
+
*/
|
|
99
|
+
function scanMcpConfig(): { config: string; risk: string }[] {
|
|
100
|
+
const risks: { config: string; risk: string }[] = []
|
|
101
|
+
const configPaths = [
|
|
102
|
+
join(OPENCLAW_DIR, 'mcp.json'),
|
|
103
|
+
join(OPENCLAW_DIR, 'config', 'mcp.json'),
|
|
104
|
+
join(OPENCLAW_DIR, 'settings.json'),
|
|
105
|
+
]
|
|
106
|
+
for (const p of configPaths) {
|
|
107
|
+
if (!existsSync(p)) continue
|
|
108
|
+
try {
|
|
109
|
+
const content = readFileSync(p, 'utf-8')
|
|
110
|
+
if (/webhook|exfil|callback|pastebin|requestbin/i.test(content)) {
|
|
111
|
+
risks.push({ config: p, risk: 'suspicious URL in config' })
|
|
112
|
+
}
|
|
113
|
+
if (/command.*:.*["'](?:curl|wget|nc)\s/i.test(content)) {
|
|
114
|
+
risks.push({ config: p, risk: 'network command in MCP' })
|
|
115
|
+
}
|
|
116
|
+
} catch { /* skip */ }
|
|
117
|
+
}
|
|
118
|
+
return risks
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 执行全部自动检查,返回结果(供启动时告警用)
|
|
123
|
+
*/
|
|
124
|
+
export async function runAutoCheck(): Promise<AutoCheckResult> {
|
|
125
|
+
const ocVersion = getOpenClawVersion()
|
|
126
|
+
const [openclawVulns, pluginRisks, mcpRisks] = await Promise.all([
|
|
127
|
+
checkOpenClawVulns(ocVersion),
|
|
128
|
+
Promise.resolve(scanPluginsQuick()),
|
|
129
|
+
Promise.resolve(scanMcpConfig()),
|
|
130
|
+
])
|
|
131
|
+
const rootWarning = typeof process.getuid === 'function' && process.getuid() === 0
|
|
132
|
+
return { openclawVulns, pluginRisks, mcpRisks, rootWarning }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 启动时执行检查,发现问题时通过 logger 告警
|
|
137
|
+
*/
|
|
138
|
+
export function runAutoCheckOnStartup(logger: { warn: (s: string) => void }, locale: 'zh' | 'en'): void {
|
|
139
|
+
runAutoCheck().then(result => {
|
|
140
|
+
const zh = locale === 'zh'
|
|
141
|
+
const lines: string[] = []
|
|
142
|
+
|
|
143
|
+
if (result.openclawVulns.length > 0) {
|
|
144
|
+
lines.push(zh ? '⚠️ OpenClaw 存在已知漏洞:' : '⚠️ OpenClaw has known vulnerabilities:')
|
|
145
|
+
for (const v of result.openclawVulns) {
|
|
146
|
+
lines.push(` ${v.id} [${v.severity}]: ${v.description}`)
|
|
147
|
+
}
|
|
148
|
+
lines.push(zh ? ' 请运行 /check-updates 查看详情并升级' : ' Run /check-updates for details and upgrade')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (result.pluginRisks.length > 0) {
|
|
152
|
+
lines.push(zh ? '⚠️ 插件扫描发现可疑模式:' : '⚠️ Plugin scan found suspicious patterns:')
|
|
153
|
+
for (const r of result.pluginRisks.slice(0, 3)) {
|
|
154
|
+
lines.push(` ${r.plugin}: ${r.risk}`)
|
|
155
|
+
}
|
|
156
|
+
if (result.pluginRisks.length > 3) {
|
|
157
|
+
lines.push(` ... 共 ${result.pluginRisks.length} 项`)
|
|
158
|
+
}
|
|
159
|
+
lines.push(zh ? ' 请运行 /scan-plugins 查看详情' : ' Run /scan-plugins for details')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result.mcpRisks.length > 0) {
|
|
163
|
+
lines.push(zh ? '⚠️ MCP 配置存在可疑项:' : '⚠️ Suspicious items in MCP config:')
|
|
164
|
+
for (const r of result.mcpRisks) {
|
|
165
|
+
lines.push(` ${r.config}: ${r.risk}`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (result.rootWarning) {
|
|
170
|
+
lines.push(zh ? '⚠️ 正在以 root 运行,建议使用普通用户' : '⚠️ Running as root, consider using non-root user')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (lines.length > 0) {
|
|
174
|
+
logger.warn('[ShellWard] 自动安全检查:\n' + lines.join('\n'))
|
|
175
|
+
}
|
|
176
|
+
}).catch(() => { /* 静默失败,不阻塞 */ })
|
|
177
|
+
}
|
package/src/commands/audit.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFileSync, statSync } from 'fs'
|
|
4
4
|
import { join } from 'path'
|
|
5
|
+
import { getHomeDir } from '../utils'
|
|
5
6
|
import type { ShellWardConfig } from '../types'
|
|
6
7
|
import { resolveLocale } from '../types'
|
|
7
8
|
|
|
8
|
-
const LOG_FILE = join(
|
|
9
|
+
const LOG_FILE = join(getHomeDir(), '.openclaw', 'shellward', 'audit.jsonl')
|
|
9
10
|
|
|
10
11
|
export function registerAuditCommand(api: any, config: ShellWardConfig) {
|
|
11
12
|
const locale = resolveLocale(config)
|
|
@@ -13,8 +14,8 @@ export function registerAuditCommand(api: any, config: ShellWardConfig) {
|
|
|
13
14
|
api.registerCommand({
|
|
14
15
|
name: 'audit',
|
|
15
16
|
description: locale === 'zh'
|
|
16
|
-
? '📋 查看 ShellWard 审计日志 (用法: /audit [数量] [block|
|
|
17
|
-
: '📋 View ShellWard audit log (usage: /audit [count] [block|
|
|
17
|
+
? '📋 查看 ShellWard 审计日志 (用法: /audit [数量] [block|audit|critical])'
|
|
18
|
+
: '📋 View ShellWard audit log (usage: /audit [count] [block|audit|critical])',
|
|
18
19
|
acceptsArgs: true,
|
|
19
20
|
handler: (ctx: any) => {
|
|
20
21
|
const zh = locale === 'zh'
|
|
@@ -43,6 +44,8 @@ export function registerAuditCommand(api: any, config: ShellWardConfig) {
|
|
|
43
44
|
// Apply filter
|
|
44
45
|
if (filter === 'block') {
|
|
45
46
|
lines = lines.filter(l => l.includes('"action":"block"'))
|
|
47
|
+
} else if (filter === 'audit') {
|
|
48
|
+
lines = lines.filter(l => l.includes('"action":"audit"'))
|
|
46
49
|
} else if (filter === 'redact') {
|
|
47
50
|
lines = lines.filter(l => l.includes('"action":"redact"'))
|
|
48
51
|
} else if (filter === 'critical') {
|
|
@@ -65,7 +68,7 @@ export function registerAuditCommand(api: any, config: ShellWardConfig) {
|
|
|
65
68
|
const formatted = recent.map(line => {
|
|
66
69
|
try {
|
|
67
70
|
const e = JSON.parse(line)
|
|
68
|
-
const icon = e.action === 'block' ? '🚫' : e.action === 'redact' ? '🔒' : e.level === 'CRITICAL' ? '🔴' : 'ℹ️'
|
|
71
|
+
const icon = e.action === 'block' ? '🚫' : e.action === 'audit' ? '📋' : e.action === 'redact' ? '🔒' : e.level === 'CRITICAL' ? '🔴' : 'ℹ️'
|
|
69
72
|
const time = e.ts?.slice(11, 19) || '??:??:??'
|
|
70
73
|
return `${icon} \`${time}\` **${e.layer}** ${e.action}: ${e.detail?.slice(0, 80) || ''}${e.pattern ? ` [${e.pattern}]` : ''}`
|
|
71
74
|
} catch {
|
|
@@ -1,34 +1,35 @@
|
|
|
1
|
-
// src/commands/check-updates.ts — /check-updates: check
|
|
1
|
+
// src/commands/check-updates.ts — /check-updates: check versions + remote vulnerability DB
|
|
2
2
|
|
|
3
3
|
import { execSync } from 'child_process'
|
|
4
4
|
import { existsSync, readFileSync } from 'fs'
|
|
5
5
|
import { join } from 'path'
|
|
6
6
|
import type { ShellWardConfig } from '../types'
|
|
7
7
|
import { resolveLocale } from '../types'
|
|
8
|
+
import { checkForUpdate, fetchVulnDB, compareVersions } from '../update-check'
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
const
|
|
10
|
+
// Local fallback vulnerability database (used when remote fetch fails)
|
|
11
|
+
// Contains only CVE-assigned vulnerabilities as minimum baseline
|
|
12
|
+
const LOCAL_VULNS = [
|
|
12
13
|
{
|
|
13
|
-
affectedBelow: '
|
|
14
|
-
severity: 'HIGH',
|
|
15
|
-
id: '
|
|
16
|
-
description_zh: '
|
|
17
|
-
description_en: '
|
|
14
|
+
affectedBelow: '1.0.111',
|
|
15
|
+
severity: 'HIGH' as const,
|
|
16
|
+
id: 'CVE-2025-59536',
|
|
17
|
+
description_zh: '远程代码执行:恶意仓库通过 Hooks 和 MCP Server 在信任提示前执行任意命令 (CVSS 8.7)',
|
|
18
|
+
description_en: 'RCE via Hooks and MCP Server bypass — arbitrary shell execution before trust dialog (CVSS 8.7)',
|
|
18
19
|
},
|
|
19
20
|
{
|
|
20
|
-
affectedBelow: '
|
|
21
|
-
severity: '
|
|
22
|
-
id: '
|
|
23
|
-
description_zh: '
|
|
24
|
-
description_en: '
|
|
21
|
+
affectedBelow: '2.0.65',
|
|
22
|
+
severity: 'MEDIUM' as const,
|
|
23
|
+
id: 'CVE-2026-21852',
|
|
24
|
+
description_zh: 'API 密钥泄露:恶意仓库通过 settings.json 设置 ANTHROPIC_BASE_URL 窃取用户 API Key (CVSS 5.3)',
|
|
25
|
+
description_en: 'API key exfiltration via ANTHROPIC_BASE_URL in settings.json before trust prompt (CVSS 5.3)',
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
|
-
affectedBelow: '2026.
|
|
28
|
-
severity: 'HIGH',
|
|
29
|
-
id: '
|
|
30
|
-
description_zh: '
|
|
31
|
-
description_en: '
|
|
28
|
+
affectedBelow: '2026.2.7',
|
|
29
|
+
severity: 'HIGH' as const,
|
|
30
|
+
id: 'GHSA-ff64-7w26-62rf',
|
|
31
|
+
description_zh: '沙箱逃逸:通过 settings.json 持久化配置注入',
|
|
32
|
+
description_en: 'Sandbox escape via persistent configuration injection in settings.json',
|
|
32
33
|
},
|
|
33
34
|
]
|
|
34
35
|
|
|
@@ -38,10 +39,10 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
|
|
|
38
39
|
api.registerCommand({
|
|
39
40
|
name: 'check-updates',
|
|
40
41
|
description: locale === 'zh'
|
|
41
|
-
? '🔄
|
|
42
|
-
: '🔄 Check
|
|
42
|
+
? '🔄 检查版本更新和已知漏洞(支持远程漏洞库)'
|
|
43
|
+
: '🔄 Check for updates and known vulnerabilities (remote vuln DB)',
|
|
43
44
|
acceptsArgs: false,
|
|
44
|
-
handler: () => {
|
|
45
|
+
handler: async () => {
|
|
45
46
|
const zh = locale === 'zh'
|
|
46
47
|
const lines: string[] = []
|
|
47
48
|
|
|
@@ -49,20 +50,19 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
|
|
|
49
50
|
lines.push('')
|
|
50
51
|
|
|
51
52
|
// 1. Get OpenClaw version
|
|
52
|
-
let
|
|
53
|
+
let openclawVersion = 'unknown'
|
|
53
54
|
try {
|
|
54
55
|
const out = execSync('openclaw --version 2>&1', { timeout: 5000 }).toString().trim()
|
|
55
|
-
// Extract version like "2026.3.8"
|
|
56
56
|
const match = out.match(/(\d{4}\.\d+\.\d+)/)
|
|
57
|
-
if (match)
|
|
57
|
+
if (match) openclawVersion = match[1]
|
|
58
58
|
} catch { /* skip */ }
|
|
59
59
|
|
|
60
60
|
lines.push(zh
|
|
61
|
-
? `### OpenClaw 版本: ${
|
|
62
|
-
: `### OpenClaw Version: ${
|
|
61
|
+
? `### OpenClaw 版本: ${openclawVersion}`
|
|
62
|
+
: `### OpenClaw Version: ${openclawVersion}`)
|
|
63
63
|
lines.push('')
|
|
64
64
|
|
|
65
|
-
// 2. Check ShellWard version
|
|
65
|
+
// 2. Check ShellWard version + available update
|
|
66
66
|
let shellwardVersion = 'unknown'
|
|
67
67
|
try {
|
|
68
68
|
const pkgPath = join(__dirname, '../../package.json')
|
|
@@ -75,17 +75,45 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
|
|
|
75
75
|
lines.push(zh
|
|
76
76
|
? `### ShellWard 版本: ${shellwardVersion}`
|
|
77
77
|
: `### ShellWard Version: ${shellwardVersion}`)
|
|
78
|
+
|
|
79
|
+
// Check for ShellWard update from npm
|
|
80
|
+
try {
|
|
81
|
+
const updateInfo = await checkForUpdate(shellwardVersion)
|
|
82
|
+
if (updateInfo?.updateAvailable) {
|
|
83
|
+
lines.push(zh
|
|
84
|
+
? ` 🆕 **新版本 v${updateInfo.latest} 可用!** 运行 \`openclaw plugins update shellward\` 更新`
|
|
85
|
+
: ` 🆕 **v${updateInfo.latest} available!** Run \`openclaw plugins update shellward\` to update`)
|
|
86
|
+
} else if (updateInfo) {
|
|
87
|
+
lines.push(zh ? ' ✅ 已是最新版本' : ' ✅ Up to date')
|
|
88
|
+
}
|
|
89
|
+
} catch { /* skip */ }
|
|
78
90
|
lines.push('')
|
|
79
91
|
|
|
80
|
-
// 3. Check known vulnerabilities
|
|
92
|
+
// 3. Check known vulnerabilities (remote DB with local fallback)
|
|
81
93
|
lines.push(zh ? '### 已知漏洞检查' : '### Known Vulnerability Check')
|
|
82
94
|
|
|
83
|
-
|
|
95
|
+
let vulnDB = LOCAL_VULNS
|
|
96
|
+
let alerts: { id: string; severity: string; date: string; description_zh: string; description_en: string }[] = []
|
|
97
|
+
let dbSource = 'local'
|
|
98
|
+
try {
|
|
99
|
+
const remote = await fetchVulnDB()
|
|
100
|
+
if (remote.vulns.length > 0) {
|
|
101
|
+
vulnDB = remote.vulns
|
|
102
|
+
dbSource = 'remote'
|
|
103
|
+
}
|
|
104
|
+
alerts = remote.alerts || []
|
|
105
|
+
} catch { /* use local */ }
|
|
106
|
+
|
|
107
|
+
lines.push(zh
|
|
108
|
+
? ` 数据源: ${dbSource === 'remote' ? `远程漏洞库 (GitHub) — ${vulnDB.length} 条记录` : '本地内置数据库'}`
|
|
109
|
+
: ` Source: ${dbSource === 'remote' ? `Remote vuln DB (GitHub) — ${vulnDB.length} entries` : 'Local built-in database'}`)
|
|
110
|
+
|
|
111
|
+
if (openclawVersion === 'unknown') {
|
|
84
112
|
lines.push(zh
|
|
85
113
|
? ' ⚠️ 无法确定 OpenClaw 版本,请手动检查'
|
|
86
114
|
: ' ⚠️ Cannot determine OpenClaw version, please check manually')
|
|
87
115
|
} else {
|
|
88
|
-
const affected =
|
|
116
|
+
const affected = vulnDB.filter(v => compareVersions(openclawVersion, v.affectedBelow) < 0)
|
|
89
117
|
if (affected.length === 0) {
|
|
90
118
|
lines.push(zh
|
|
91
119
|
? ' ✅ 当前版本未发现已知漏洞'
|
|
@@ -96,11 +124,22 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
|
|
|
96
124
|
const desc = zh ? vuln.description_zh : vuln.description_en
|
|
97
125
|
lines.push(` ${icon} **${vuln.id}** [${vuln.severity}]: ${desc}`)
|
|
98
126
|
lines.push(zh
|
|
99
|
-
? ` 影响版本: < ${vuln.affectedBelow} —
|
|
100
|
-
: ` Affected: < ${vuln.affectedBelow} — please upgrade
|
|
127
|
+
? ` 影响版本: < ${vuln.affectedBelow} — 请升级`
|
|
128
|
+
: ` Affected: < ${vuln.affectedBelow} — please upgrade`)
|
|
101
129
|
}
|
|
102
130
|
}
|
|
103
131
|
}
|
|
132
|
+
|
|
133
|
+
// Supply chain alerts
|
|
134
|
+
if (alerts.length > 0) {
|
|
135
|
+
lines.push('')
|
|
136
|
+
lines.push(zh ? '### 供应链安全警告' : '### Supply Chain Alerts')
|
|
137
|
+
for (const alert of alerts) {
|
|
138
|
+
const icon = alert.severity === 'CRITICAL' ? '🔴' : '🟡'
|
|
139
|
+
const desc = zh ? alert.description_zh : alert.description_en
|
|
140
|
+
lines.push(` ${icon} **${alert.id}** [${alert.date}]: ${desc}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
104
143
|
lines.push('')
|
|
105
144
|
|
|
106
145
|
// 4. Check Node.js version
|
|
@@ -135,17 +174,3 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
|
|
|
135
174
|
},
|
|
136
175
|
})
|
|
137
176
|
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Compare two version strings like "2026.3.8" vs "2026.3.6"
|
|
141
|
-
* Returns: negative if a < b, 0 if equal, positive if a > b
|
|
142
|
-
*/
|
|
143
|
-
function compareVersions(a: string, b: string): number {
|
|
144
|
-
const pa = a.split('.').map(Number)
|
|
145
|
-
const pb = b.split('.').map(Number)
|
|
146
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
147
|
-
const diff = (pa[i] || 0) - (pb[i] || 0)
|
|
148
|
-
if (diff !== 0) return diff
|
|
149
|
-
}
|
|
150
|
-
return 0
|
|
151
|
-
}
|
package/src/commands/harden.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { execSync } from 'child_process'
|
|
|
6
6
|
import type { ShellWardConfig } from '../types'
|
|
7
7
|
import { resolveLocale } from '../types'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { getHomeDir } from '../utils'
|
|
10
|
+
const HOME = getHomeDir()
|
|
10
11
|
|
|
11
12
|
export function registerHardenCommand(api: any, config: ShellWardConfig) {
|
|
12
13
|
const locale = resolveLocale(config)
|
|
@@ -171,6 +172,43 @@ export function registerHardenCommand(api: any, config: ShellWardConfig) {
|
|
|
171
172
|
}
|
|
172
173
|
lines.push('')
|
|
173
174
|
|
|
175
|
+
// === 5. One-click scripts ===
|
|
176
|
+
lines.push(zh ? '### 一键安全脚本' : '### One-click Security Scripts')
|
|
177
|
+
|
|
178
|
+
// Dockerfile
|
|
179
|
+
lines.push(zh ? '**容器隔离** — 复制以下 Dockerfile:' : '**Container isolation** — copy this Dockerfile:')
|
|
180
|
+
lines.push('```dockerfile')
|
|
181
|
+
lines.push('FROM node:22-slim')
|
|
182
|
+
lines.push('RUN useradd -m -s /bin/bash openclaw')
|
|
183
|
+
lines.push('USER openclaw')
|
|
184
|
+
lines.push('WORKDIR /home/openclaw')
|
|
185
|
+
lines.push('RUN npm install -g openclaw')
|
|
186
|
+
lines.push('COPY .env .env')
|
|
187
|
+
lines.push('EXPOSE 19000')
|
|
188
|
+
lines.push('CMD ["openclaw", "agent", "--local"]')
|
|
189
|
+
lines.push('```')
|
|
190
|
+
lines.push(zh
|
|
191
|
+
? '运行: `docker build -t openclaw-safe . && docker run --rm -it openclaw-safe`'
|
|
192
|
+
: 'Run: `docker build -t openclaw-safe . && docker run --rm -it openclaw-safe`')
|
|
193
|
+
lines.push('')
|
|
194
|
+
|
|
195
|
+
// Firewall
|
|
196
|
+
lines.push(zh ? '**防火墙限制** — 仅允许必要出站:' : '**Firewall** — allow only necessary outbound:')
|
|
197
|
+
lines.push('```bash')
|
|
198
|
+
lines.push('# 只允许 HTTPS 出站(API 调用),禁止其他出站')
|
|
199
|
+
lines.push('sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT')
|
|
200
|
+
lines.push('sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT')
|
|
201
|
+
lines.push('sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT # DNS')
|
|
202
|
+
lines.push('sudo iptables -A OUTPUT -o lo -j ACCEPT # localhost')
|
|
203
|
+
lines.push('sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT')
|
|
204
|
+
lines.push('sudo iptables -A OUTPUT -j LOG --log-prefix "BLOCKED: "')
|
|
205
|
+
lines.push('sudo iptables -A OUTPUT -j DROP')
|
|
206
|
+
lines.push('```')
|
|
207
|
+
lines.push(zh
|
|
208
|
+
? '⚠️ 执行前请确认不会影响其他服务'
|
|
209
|
+
: '⚠️ Review before applying — may affect other services')
|
|
210
|
+
lines.push('')
|
|
211
|
+
|
|
174
212
|
// === Summary ===
|
|
175
213
|
lines.push('---')
|
|
176
214
|
if (issues.length === 0) {
|
package/src/commands/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { registerAuditCommand } from './audit'
|
|
|
7
7
|
import { registerHardenCommand } from './harden'
|
|
8
8
|
import { registerScanPluginsCommand } from './scan-plugins'
|
|
9
9
|
import { registerCheckUpdatesCommand } from './check-updates'
|
|
10
|
+
import { registerUpgradeOpenClawCommand } from './upgrade-openclaw'
|
|
10
11
|
|
|
11
12
|
export function registerAllCommands(api: any, config: ShellWardConfig) {
|
|
12
13
|
const locale = resolveLocale(config)
|
|
@@ -17,6 +18,7 @@ export function registerAllCommands(api: any, config: ShellWardConfig) {
|
|
|
17
18
|
registerHardenCommand(api, config)
|
|
18
19
|
registerScanPluginsCommand(api, config)
|
|
19
20
|
registerCheckUpdatesCommand(api, config)
|
|
21
|
+
registerUpgradeOpenClawCommand(api, config)
|
|
20
22
|
|
|
21
23
|
// Register /cg shortcut with help
|
|
22
24
|
api.registerCommand({
|
|
@@ -31,23 +33,25 @@ export function registerAllCommands(api: any, config: ShellWardConfig) {
|
|
|
31
33
|
| 命令 | 说明 |
|
|
32
34
|
|------|------|
|
|
33
35
|
| \`/security\` | 安全状态总览(防御层、审计统计、系统检查) |
|
|
34
|
-
| \`/audit [数量] [过滤]\` | 查看审计日志 (过滤: block/
|
|
36
|
+
| \`/audit [数量] [过滤]\` | 查看审计日志 (过滤: block/audit/critical/high) |
|
|
35
37
|
| \`/harden\` | 安全扫描 · \`/harden fix\` 自动修复权限 |
|
|
36
38
|
| \`/scan-plugins\` | 扫描已安装插件的安全风险 |
|
|
37
39
|
| \`/check-updates\` | 检查 OpenClaw 版本和已知漏洞 |
|
|
40
|
+
| \`/upgrade-openclaw\` | 一键升级 OpenClaw · \`/upgrade-openclaw yes\` 直接执行 |
|
|
38
41
|
|
|
39
42
|
**当前防御层 (8层):**
|
|
40
|
-
L1 提示注入 · L2
|
|
41
|
-
L5 安全门 · L6
|
|
43
|
+
L1 提示注入 · L2 输出审计 · L3 工具拦截 · L4 注入检测
|
|
44
|
+
L5 安全门 · L6 回复审计 · L7 数据流监控 · L8 会话安全`
|
|
42
45
|
: `🛡️ **ShellWard Quick Commands**
|
|
43
46
|
|
|
44
47
|
| Command | Description |
|
|
45
48
|
|---------|-------------|
|
|
46
49
|
| \`/security\` | Security status overview (layers, audit stats, system checks) |
|
|
47
|
-
| \`/audit [count] [filter]\` | View audit log (filter: block/
|
|
50
|
+
| \`/audit [count] [filter]\` | View audit log (filter: block/audit/critical/high) |
|
|
48
51
|
| \`/harden\` | Security scan · \`/harden fix\` to auto-fix permissions |
|
|
49
52
|
| \`/scan-plugins\` | Scan installed plugins for security risks |
|
|
50
53
|
| \`/check-updates\` | Check OpenClaw version and known vulnerabilities |
|
|
54
|
+
| \`/upgrade-openclaw\` | Upgrade OpenClaw · \`/upgrade-openclaw yes\` to execute |
|
|
51
55
|
|
|
52
56
|
**Active Defense Layers (8):**
|
|
53
57
|
L1 Prompt Guard · L2 Output Scanner · L3 Tool Blocker · L4 Input Auditor
|
|
@@ -5,7 +5,8 @@ import { join } from 'path'
|
|
|
5
5
|
import type { ShellWardConfig } from '../types'
|
|
6
6
|
import { resolveLocale } from '../types'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import { getHomeDir } from '../utils'
|
|
9
|
+
const HOME = getHomeDir()
|
|
9
10
|
const OPENCLAW_DIR = join(HOME, '.openclaw')
|
|
10
11
|
|
|
11
12
|
// Known suspicious patterns in plugin code
|
|
@@ -74,6 +75,7 @@ export function registerScanPluginsCommand(api: any, config: ShellWardConfig) {
|
|
|
74
75
|
lines.push('')
|
|
75
76
|
|
|
76
77
|
let totalRisks = 0
|
|
78
|
+
const riskyPluginNames = new Set<string>()
|
|
77
79
|
|
|
78
80
|
for (const plugin of pluginDirs) {
|
|
79
81
|
const risks: string[] = []
|
|
@@ -113,9 +115,9 @@ export function registerScanPluginsCommand(api: any, config: ShellWardConfig) {
|
|
|
113
115
|
try {
|
|
114
116
|
const content = readFileSync(file, 'utf-8')
|
|
115
117
|
for (const rule of SUSPICIOUS_PATTERNS) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
// Use fresh regex to avoid lastIndex state issues with global patterns
|
|
119
|
+
const regex = new RegExp(rule.pattern.source, rule.pattern.flags)
|
|
120
|
+
if (regex.test(content)) {
|
|
119
121
|
const relPath = file.replace(plugin.path + '/', '')
|
|
120
122
|
risks.push(zh
|
|
121
123
|
? `⚠️ ${relPath}: ${rule.name} (${rule.risk})`
|
|
@@ -147,11 +149,14 @@ export function registerScanPluginsCommand(api: any, config: ShellWardConfig) {
|
|
|
147
149
|
lines.push(` ${risk}`)
|
|
148
150
|
totalRisks++
|
|
149
151
|
}
|
|
152
|
+
if (risks.some(r => r.startsWith('🔴') || (r.startsWith('⚠️') && !r.includes('签名') && !r.includes('signature') && !r.includes('依赖') && !r.includes('deps')))) {
|
|
153
|
+
riskyPluginNames.add(plugin.name)
|
|
154
|
+
}
|
|
150
155
|
}
|
|
151
156
|
lines.push('')
|
|
152
157
|
}
|
|
153
158
|
|
|
154
|
-
// Summary
|
|
159
|
+
// Summary + removal commands
|
|
155
160
|
lines.push('---')
|
|
156
161
|
if (totalRisks === 0) {
|
|
157
162
|
lines.push(zh ? '✅ **所有插件扫描通过**' : '✅ **All plugins passed scan**')
|
|
@@ -159,6 +164,17 @@ export function registerScanPluginsCommand(api: any, config: ShellWardConfig) {
|
|
|
159
164
|
lines.push(zh
|
|
160
165
|
? `⚠️ **发现 ${totalRisks} 个潜在风险** — 请审查标记的插件`
|
|
161
166
|
: `⚠️ **${totalRisks} potential risks found** — review flagged plugins`)
|
|
167
|
+
|
|
168
|
+
if (riskyPluginNames.size > 0) {
|
|
169
|
+
lines.push('')
|
|
170
|
+
lines.push(zh ? '**一键移除高风险插件** — 复制执行:' : '**Remove risky plugins** — copy & run:')
|
|
171
|
+
lines.push('```bash')
|
|
172
|
+
for (const name of riskyPluginNames) {
|
|
173
|
+
lines.push(`openclaw plugins uninstall ${name}`)
|
|
174
|
+
}
|
|
175
|
+
lines.push('```')
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
lines.push(zh
|
|
163
179
|
? '💡 建议: 仅从可信渠道安装插件,定期运行 `/scan-plugins` 检查'
|
|
164
180
|
: '💡 Tip: Only install plugins from trusted sources, run `/scan-plugins` regularly')
|
package/src/commands/security.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import { readFileSync, statSync, existsSync, readdirSync } from 'fs'
|
|
4
4
|
import { join } from 'path'
|
|
5
5
|
import { execSync } from 'child_process'
|
|
6
|
+
import { getHomeDir } from '../utils'
|
|
6
7
|
import type { ShellWardConfig } from '../types'
|
|
7
8
|
import { resolveLocale } from '../types'
|
|
8
9
|
|
|
9
|
-
const LOG_DIR = join(
|
|
10
|
+
const LOG_DIR = join(getHomeDir(), '.openclaw', 'shellward')
|
|
10
11
|
const LOG_FILE = join(LOG_DIR, 'audit.jsonl')
|
|
11
12
|
|
|
12
13
|
export function registerSecurityCommand(api: any, config: ShellWardConfig) {
|
|
@@ -33,6 +34,9 @@ export function registerSecurityCommand(api: any, config: ShellWardConfig) {
|
|
|
33
34
|
['L3 Tool Blocker', config.layers.toolBlocker],
|
|
34
35
|
['L4 Input Auditor', config.layers.inputAuditor],
|
|
35
36
|
['L5 Security Gate', config.layers.securityGate],
|
|
37
|
+
['L6 Outbound Guard', config.layers.outboundGuard],
|
|
38
|
+
['L7 Data Flow Guard', config.layers.dataFlowGuard],
|
|
39
|
+
['L8 Session Guard', config.layers.sessionGuard],
|
|
36
40
|
]
|
|
37
41
|
for (const [name, enabled] of layers) {
|
|
38
42
|
lines.push(` ${enabled ? '✅' : '❌'} ${name}`)
|
|
@@ -54,15 +58,15 @@ export function registerSecurityCommand(api: any, config: ShellWardConfig) {
|
|
|
54
58
|
const allLines = content.trim().split('\n').filter(Boolean)
|
|
55
59
|
const total = allLines.length
|
|
56
60
|
let blocks = 0
|
|
57
|
-
let
|
|
61
|
+
let audits = 0
|
|
58
62
|
let criticals = 0
|
|
59
63
|
for (const line of allLines) {
|
|
60
64
|
if (line.includes('"action":"block"')) blocks++
|
|
61
|
-
if (line.includes('"action":"
|
|
65
|
+
if (line.includes('"action":"audit"')) audits++
|
|
62
66
|
if (line.includes('"level":"CRITICAL"')) criticals++
|
|
63
67
|
}
|
|
64
68
|
lines.push(` ${zh ? '总事件' : 'Total events'}: ${total}`)
|
|
65
|
-
lines.push(` ${zh ? '拦截' : 'Blocked'}: ${blocks} | ${zh ? '
|
|
69
|
+
lines.push(` ${zh ? '拦截' : 'Blocked'}: ${blocks} | ${zh ? '审计' : 'Audited'}: ${audits} | ${zh ? '严重' : 'Critical'}: ${criticals}`)
|
|
66
70
|
} catch {
|
|
67
71
|
lines.push(zh ? ' ⚠️ 日志文件不存在' : ' ⚠️ Log file not found')
|
|
68
72
|
}
|