Skip to content

Commit 8f51490

Browse files
jnMetaCodeclaude
andcommitted
v0.4.0: hook error handling, update notifications, remote vuln DB
Major changes: - Add createSafeApi() wrapper: try-catch on all 8 defense layer hooks, fail-safe (block on security hooks, pass on others) - Add non-blocking version update check with notification dedup (same version only notified once, 24h check interval) - Add remote vulnerability database (17 real CVEs/GHSAs + 1 supply chain alert) with 24h cache and local fallback - Fix ReDoS in email regex (333x speedup on 200KB text) - Fix fork bomb regex broken by splitCommands - Fix injection gaps: expand zh_new_role/zh_no_restriction rules, add zh_mixed_lang_injection rule (26 total injection rules) - Add defensive type conversion for non-string toolName/params - Fix scan-plugins regex global flag state pollution - 100 tests passing (37 integration + 42 edge cases + 21 update check) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bcced6a commit 8f51490

19 files changed

+953
-178
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ ShellWard protects your OpenClaw agent with 8 defense layers:
3030
- **Bilingual** — all messages, rules, and prompts in English and Chinese
3131
- **Chinese PII detection** — ID card (with checksum validation), phone number, bank card (Luhn)
3232
- **Global PII detection** — API keys, JWT, passwords, US SSN, credit cards, emails
33-
- **25 injection rules**13 Chinese + 12 English patterns with risk scoring
33+
- **26 injection rules**14 Chinese + 12 English patterns with risk scoring
3434
- **15 dangerous command rules** — fork bombs, reverse shells, disk formatting, etc. (all case-insensitive)
3535
- **12 protected path rules** — .env, .ssh, private keys, cloud credentials
3636
- **Dual mode**`enforce` (block + log) or `audit` (log only)
@@ -230,7 +230,7 @@ ShellWard 通过 8 层防御保护你的 OpenClaw 智能体:
230230
- **中英双语** — 所有消息、规则、提示均支持中英文
231231
- **中国 PII 检测** — 身份证号(含校验位验证)、手机号、银行卡号(Luhn 校验)
232232
- **国际 PII 检测** — API Key、JWT、密码、美国 SSN、信用卡、邮箱
233-
- **25 条注入规则**13 条中文 + 12 条英文,带风险评分
233+
- **26 条注入规则**14 条中文 + 12 条英文,带风险评分
234234
- **双模式**`enforce`(拦截+记录)或 `audit`(仅记录)
235235
- **JSONL 审计日志** — 零依赖、支持 grep/jq 查询、100MB 自动轮转
236236

openclaw.plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "shellward",
33
"name": "ShellWard",
44
"description": "First bilingual (EN/ZH) security plugin for OpenClaw — injection detection, dangerous operation blocking, PII/secret redaction (incl. Chinese ID card, phone, bank card), audit logging",
5-
"version": "0.3.4",
5+
"version": "0.4.0",
66
"skills": ["./skills"],
77
"configSchema": {
88
"type": "object",

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "shellward",
3-
"version": "0.3.4",
3+
"version": "0.4.0",
44
"description": "First bilingual (EN/ZH) security plugin for OpenClaw — Chinese PII detection (ID card/phone/bank card), prompt injection detection (13 ZH + 12 EN rules), dangerous command blocking, audit logging. Zero dependencies.",
55
"keywords": [
66
"shellward",
@@ -31,6 +31,7 @@
3131
"src/",
3232
"skills/",
3333
"openclaw.plugin.json",
34+
"vuln-db.json",
3435
"install.sh",
3536
"install.ps1",
3637
"LICENSE",

src/commands/check-updates.ts

Lines changed: 72 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,35 @@
1-
// src/commands/check-updates.ts — /check-updates: check OpenClaw version and known vulnerabilities
1+
// src/commands/check-updates.ts — /check-updates: check versions + remote vulnerability DB
22

33
import { execSync } from 'child_process'
44
import { existsSync, readFileSync } from 'fs'
55
import { join } from 'path'
66
import type { ShellWardConfig } from '../types'
77
import { resolveLocale } from '../types'
8+
import { checkForUpdate, fetchVulnDB, compareVersions } from '../update-check'
89

9-
// Known vulnerability database (hardcoded, updated with plugin releases)
10-
// Format: { version_range, severity, cve, description_zh, description_en }
11-
const KNOWN_VULNS = [
10+
// Local fallback vulnerability database (used when remote fetch fails)
11+
// Contains only CVE-assigned vulnerabilities as minimum baseline
12+
const LOCAL_VULNS = [
1213
{
13-
affectedBelow: '2026.3.6',
14-
severity: 'HIGH',
15-
id: 'CG-2026-001',
16-
description_zh: 'tool_result_persist hook 可被绕过泄露敏感数据',
17-
description_en: 'tool_result_persist hook bypass may leak sensitive data',
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)',
1819
},
1920
{
20-
affectedBelow: '2026.3.4',
21-
severity: 'CRITICAL',
22-
id: 'CG-2026-002',
23-
description_zh: '插件系统缺少签名验证,可加载恶意插件',
24-
description_en: 'Plugin system lacks signature verification, allows malicious plugins',
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)',
2526
},
2627
{
27-
affectedBelow: '2026.3.2',
28-
severity: 'HIGH',
29-
id: 'CG-2026-003',
30-
description_zh: 'Gateway 默认绑定 0.0.0.0,未认证即可远程执行',
31-
description_en: 'Gateway binds 0.0.0.0 by default, allows unauthenticated remote execution',
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',
3233
},
3334
]
3435

@@ -38,31 +39,30 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
3839
api.registerCommand({
3940
name: 'check-updates',
4041
description: locale === 'zh'
41-
? '🔄 检查 OpenClaw 版本和已知漏洞'
42-
: '🔄 Check OpenClaw version and known vulnerabilities',
42+
? '🔄 检查版本更新和已知漏洞(支持远程漏洞库)'
43+
: '🔄 Check for updates and known vulnerabilities (remote vuln DB)',
4344
acceptsArgs: false,
44-
handler: () => {
45+
handler: async () => {
4546
const zh = locale === 'zh'
4647
const lines: string[] = []
4748

4849
lines.push(zh ? '🔄 **版本与漏洞检查**' : '🔄 **Version & Vulnerability Check**')
4950
lines.push('')
5051

5152
// 1. Get OpenClaw version
52-
let currentVersion = 'unknown'
53+
let openclawVersion = 'unknown'
5354
try {
5455
const out = execSync('openclaw --version 2>&1', { timeout: 5000 }).toString().trim()
55-
// Extract version like "2026.3.8"
5656
const match = out.match(/(\d{4}\.\d+\.\d+)/)
57-
if (match) currentVersion = match[1]
57+
if (match) openclawVersion = match[1]
5858
} catch { /* skip */ }
5959

6060
lines.push(zh
61-
? `### OpenClaw 版本: ${currentVersion}`
62-
: `### OpenClaw Version: ${currentVersion}`)
61+
? `### OpenClaw 版本: ${openclawVersion}`
62+
: `### OpenClaw Version: ${openclawVersion}`)
6363
lines.push('')
6464

65-
// 2. Check ShellWard version
65+
// 2. Check ShellWard version + available update
6666
let shellwardVersion = 'unknown'
6767
try {
6868
const pkgPath = join(__dirname, '../../package.json')
@@ -75,17 +75,45 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
7575
lines.push(zh
7676
? `### ShellWard 版本: ${shellwardVersion}`
7777
: `### 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 */ }
7890
lines.push('')
7991

80-
// 3. Check known vulnerabilities
92+
// 3. Check known vulnerabilities (remote DB with local fallback)
8193
lines.push(zh ? '### 已知漏洞检查' : '### Known Vulnerability Check')
8294

83-
if (currentVersion === 'unknown') {
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') {
84112
lines.push(zh
85113
? ' ⚠️ 无法确定 OpenClaw 版本,请手动检查'
86114
: ' ⚠️ Cannot determine OpenClaw version, please check manually')
87115
} else {
88-
const affected = KNOWN_VULNS.filter(v => compareVersions(currentVersion, v.affectedBelow) < 0)
116+
const affected = vulnDB.filter(v => compareVersions(openclawVersion, v.affectedBelow) < 0)
89117
if (affected.length === 0) {
90118
lines.push(zh
91119
? ' ✅ 当前版本未发现已知漏洞'
@@ -96,11 +124,22 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
96124
const desc = zh ? vuln.description_zh : vuln.description_en
97125
lines.push(` ${icon} **${vuln.id}** [${vuln.severity}]: ${desc}`)
98126
lines.push(zh
99-
? ` 影响版本: < ${vuln.affectedBelow} — 请升级 OpenClaw`
100-
: ` Affected: < ${vuln.affectedBelow} — please upgrade OpenClaw`)
127+
? ` 影响版本: < ${vuln.affectedBelow} — 请升级`
128+
: ` Affected: < ${vuln.affectedBelow} — please upgrade`)
101129
}
102130
}
103131
}
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+
}
104143
lines.push('')
105144

106145
// 4. Check Node.js version
@@ -135,17 +174,3 @@ export function registerCheckUpdatesCommand(api: any, config: ShellWardConfig) {
135174
},
136175
})
137176
}
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-
}

src/commands/scan-plugins.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ export function registerScanPluginsCommand(api: any, config: ShellWardConfig) {
113113
try {
114114
const content = readFileSync(file, 'utf-8')
115115
for (const rule of SUSPICIOUS_PATTERNS) {
116-
if (rule.pattern.test(content)) {
117-
// Reset lastIndex for global regexes
118-
rule.pattern.lastIndex = 0
116+
// Use fresh regex to avoid lastIndex state issues with global patterns
117+
const regex = new RegExp(rule.pattern.source, rule.pattern.flags)
118+
if (regex.test(content)) {
119119
const relPath = file.replace(plugin.path + '/', '')
120120
risks.push(zh
121121
? `⚠️ ${relPath}: ${rule.name} (${rule.risk})`

src/index.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// src/index.ts — ShellWard plugin entry point (v0.3.1)
1+
// src/index.ts — ShellWard plugin entry point (v0.4.0)
22
// 8 defense layers + 6 slash commands + 1 security skill
33

44
import { AuditLog } from './audit-log'
@@ -12,8 +12,46 @@ import { setupDataFlowGuard } from './layers/data-flow-guard'
1212
import { setupSessionGuard } from './layers/session-guard'
1313
import { registerAllCommands } from './commands/index'
1414
import { DEFAULT_CONFIG, resolveLocale } from './types'
15+
import { checkForUpdate } from './update-check'
1516
import type { ShellWardConfig } from './types'
1617

18+
const CURRENT_VERSION = '0.4.0'
19+
20+
/**
21+
* Wrap api.on so every hook handler gets try-catch protection.
22+
* If a security hook throws, we log the error and fail-safe:
23+
* - before_tool_call: block (deny on error, safer than allow)
24+
* - other hooks: return undefined (don't break the chain)
25+
*/
26+
function createSafeApi(api: any, log: AuditLog): any {
27+
return {
28+
...api,
29+
on(hookName: string, handler: Function, opts?: any) {
30+
const isBlockHook = hookName === 'before_tool_call'
31+
const wrappedHandler = (event: any) => {
32+
try {
33+
return handler(event)
34+
} catch (err: any) {
35+
const msg = err?.message || String(err)
36+
log.write({
37+
level: 'CRITICAL',
38+
layer: 'L0',
39+
action: 'error',
40+
detail: `Hook ${opts?.name || hookName} threw: ${msg.slice(0, 200)}`,
41+
})
42+
try { api.logger.warn(`[ShellWard] Hook error in ${opts?.name || hookName}: ${msg}`) } catch {}
43+
// Fail-safe: block on security hooks, pass on others
44+
if (isBlockHook) {
45+
return { block: true, blockReason: `⚠️ [ShellWard] Internal error in security check — operation blocked for safety` }
46+
}
47+
return undefined
48+
}
49+
}
50+
api.on(hookName, wrappedHandler, opts)
51+
},
52+
}
53+
}
54+
1755
function mergeConfig(userConfig: Partial<ShellWardConfig> | undefined): ShellWardConfig {
1856
if (!userConfig) return { ...DEFAULT_CONFIG }
1957

@@ -49,52 +87,54 @@ export default {
4987
const log = new AuditLog(config)
5088
const enforce = config.mode === 'enforce'
5189
const locale = resolveLocale(config)
90+
const safe = createSafeApi(api, log)
5291

5392
const modeLabel = locale === 'zh'
5493
? `模式: ${config.mode}`
5594
: `mode: ${config.mode}`
5695
api.logger.info(`[ShellWard] Security plugin started (${modeLabel})`)
5796

5897
// === Defense Layers (L1-L8) ===
98+
// All layers use `safe` wrapper — hooks get automatic try-catch + fail-safe
5999

60100
// L1: Prompt Guard (before_prompt_build — prependSystemContext for caching)
61101
if (config.layers.promptGuard) {
62-
setupPromptGuard(api, config, log)
102+
setupPromptGuard(safe, config, log)
63103
}
64104

65105
// L2: Output Scanner (tool_result_persist — redact PII in tool results)
66106
if (config.layers.outputScanner) {
67-
setupOutputScanner(api, config, log, enforce)
107+
setupOutputScanner(safe, config, log, enforce)
68108
}
69109

70110
// L3: Tool Blocker (before_tool_call — block dangerous commands/paths)
71111
if (config.layers.toolBlocker) {
72-
setupToolBlocker(api, config, log, enforce)
112+
setupToolBlocker(safe, config, log, enforce)
73113
}
74114

75115
// L4: Input Auditor (before_tool_call + message_received — injection detection)
76116
if (config.layers.inputAuditor) {
77-
setupInputAuditor(api, config, log, enforce)
117+
setupInputAuditor(safe, config, log, enforce)
78118
}
79119

80-
// L5: Security Gate (registerTool — defense in depth)
120+
// L5: Security Gate (registerTool — defense in depth, uses raw api for registerTool)
81121
if (config.layers.securityGate) {
82122
setupSecurityGate(api, config, log, enforce)
83123
}
84124

85125
// L6: Outbound Guard (message_sending — redact PII in LLM responses + canary detection)
86126
if (config.layers.outboundGuard) {
87-
setupOutboundGuard(api, config, log, enforce)
127+
setupOutboundGuard(safe, config, log, enforce)
88128
}
89129

90130
// L7: Data Flow Guard (after_tool_call + before_tool_call — anti-exfiltration)
91131
if (config.layers.dataFlowGuard) {
92-
setupDataFlowGuard(api, config, log, enforce)
132+
setupDataFlowGuard(safe, config, log, enforce)
93133
}
94134

95135
// L8: Session Guard (session_end + subagent_spawning — lifecycle security)
96136
if (config.layers.sessionGuard) {
97-
setupSessionGuard(api, config, log, enforce)
137+
setupSessionGuard(safe, config, log, enforce)
98138
}
99139

100140
// === Slash Commands ===
@@ -113,7 +153,18 @@ export default {
113153
level: 'INFO',
114154
layer: 'L1',
115155
action: 'allow',
116-
detail: `ShellWard v0.3.4 started with ${enabledCount} layers`,
156+
detail: `ShellWard v${CURRENT_VERSION} started with ${enabledCount} layers`,
117157
})
158+
159+
// === Non-blocking update check (async, won't delay startup) ===
160+
// Only notifies ONCE per new version — won't repeat after user has seen it
161+
checkForUpdate(CURRENT_VERSION).then(result => {
162+
if (result?.shouldNotify) {
163+
const msg = locale === 'zh'
164+
? `[ShellWard] 新版本 v${result.latest} 可用 (当前 v${result.current})。运行 \`openclaw plugins update shellward\` 更新`
165+
: `[ShellWard] Update available: v${result.latest} (current v${result.current}). Run \`openclaw plugins update shellward\` to update`
166+
api.logger.warn(msg)
167+
}
168+
}).catch(() => { /* silently ignore network errors */ })
118169
},
119170
}

0 commit comments

Comments
 (0)