shellward 0.3.3 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # ShellWard
2
2
 
3
- **First bilingual (EN/ZH) security plugin for OpenClaw** — prompt injection detection, dangerous operation blocking, PII/secret redaction, audit logging.
3
+ **First bilingual (EN/ZH) security plugin for OpenClaw** — the only plugin with Chinese prompt injection detection & Chinese PII redaction (ID card, phone, bank card). 8 defense layers, zero dependencies.
4
4
 
5
5
  [中文说明](#中文说明) | [English](#english)
6
6
 
@@ -30,7 +30,7 @@ ShellWard protects your OpenClaw agent with 8 defense layers:
30
30
  - **Bilingual** — all messages, rules, and prompts in English and Chinese
31
31
  - **Chinese PII detection** — ID card (with checksum validation), phone number, bank card (Luhn)
32
32
  - **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
34
34
  - **15 dangerous command rules** — fork bombs, reverse shells, disk formatting, etc. (all case-insensitive)
35
35
  - **12 protected path rules** — .env, .ssh, private keys, cloud credentials
36
36
  - **Dual mode** — `enforce` (block + log) or `audit` (log only)
@@ -230,7 +230,7 @@ ShellWard 通过 8 层防御保护你的 OpenClaw 智能体:
230
230
  - **中英双语** — 所有消息、规则、提示均支持中英文
231
231
  - **中国 PII 检测** — 身份证号(含校验位验证)、手机号、银行卡号(Luhn 校验)
232
232
  - **国际 PII 检测** — API Key、JWT、密码、美国 SSN、信用卡、邮箱
233
- - **25 条注入规则** — 13 条中文 + 12 条英文,带风险评分
233
+ - **26 条注入规则** — 14 条中文 + 12 条英文,带风险评分
234
234
  - **双模式** — `enforce`(拦截+记录)或 `audit`(仅记录)
235
235
  - **JSONL 审计日志** — 零依赖、支持 grep/jq 查询、100MB 自动轮转
236
236
 
@@ -2,7 +2,7 @@
2
2
  "id": "shellward",
3
3
  "name": "ShellWard",
4
4
  "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.3",
5
+ "version": "0.4.0",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.3.3",
4
- "description": "First bilingual (EN/ZH) security plugin for OpenClaw — injection detection, dangerous operation blocking, PII/secret redaction, audit logging",
3
+ "version": "0.4.0",
4
+ "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.",
5
5
  "keywords": [
6
6
  "shellward",
7
7
  "openclaw",
@@ -31,6 +31,7 @@
31
31
  "src/",
32
32
  "skills/",
33
33
  "openclaw.plugin.json",
34
+ "vuln-db.json",
34
35
  "install.sh",
35
36
  "install.ps1",
36
37
  "LICENSE",
@@ -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
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
- // 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 = [
12
13
  {
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)',
18
19
  },
19
20
  {
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)',
25
26
  },
26
27
  {
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',
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
- ? '🔄 检查 OpenClaw 版本和已知漏洞'
42
- : '🔄 Check OpenClaw version and known vulnerabilities',
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 currentVersion = 'unknown'
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) currentVersion = match[1]
57
+ if (match) openclawVersion = match[1]
58
58
  } catch { /* skip */ }
59
59
 
60
60
  lines.push(zh
61
- ? `### OpenClaw 版本: ${currentVersion}`
62
- : `### OpenClaw Version: ${currentVersion}`)
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
- 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') {
84
112
  lines.push(zh
85
113
  ? ' ⚠️ 无法确定 OpenClaw 版本,请手动检查'
86
114
  : ' ⚠️ Cannot determine OpenClaw version, please check manually')
87
115
  } else {
88
- const affected = KNOWN_VULNS.filter(v => compareVersions(currentVersion, v.affectedBelow) < 0)
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} — 请升级 OpenClaw`
100
- : ` Affected: < ${vuln.affectedBelow} — please upgrade OpenClaw`)
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
- }
@@ -113,9 +113,9 @@ export function registerScanPluginsCommand(api: any, config: ShellWardConfig) {
113
113
  try {
114
114
  const content = readFileSync(file, 'utf-8')
115
115
  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)) {
119
119
  const relPath = file.replace(plugin.path + '/', '')
120
120
  risks.push(zh
121
121
  ? `⚠️ ${relPath}: ${rule.name} (${rule.risk})`
package/src/index.ts CHANGED
@@ -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)
2
2
  // 8 defense layers + 6 slash commands + 1 security skill
3
3
 
4
4
  import { AuditLog } from './audit-log'
@@ -12,8 +12,46 @@ import { setupDataFlowGuard } from './layers/data-flow-guard'
12
12
  import { setupSessionGuard } from './layers/session-guard'
13
13
  import { registerAllCommands } from './commands/index'
14
14
  import { DEFAULT_CONFIG, resolveLocale } from './types'
15
+ import { checkForUpdate } from './update-check'
15
16
  import type { ShellWardConfig } from './types'
16
17
 
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
+
17
55
  function mergeConfig(userConfig: Partial<ShellWardConfig> | undefined): ShellWardConfig {
18
56
  if (!userConfig) return { ...DEFAULT_CONFIG }
19
57
 
@@ -49,6 +87,7 @@ export default {
49
87
  const log = new AuditLog(config)
50
88
  const enforce = config.mode === 'enforce'
51
89
  const locale = resolveLocale(config)
90
+ const safe = createSafeApi(api, log)
52
91
 
53
92
  const modeLabel = locale === 'zh'
54
93
  ? `模式: ${config.mode}`
@@ -56,45 +95,46 @@ export default {
56
95
  api.logger.info(`[ShellWard] Security plugin started (${modeLabel})`)
57
96
 
58
97
  // === Defense Layers (L1-L8) ===
98
+ // All layers use `safe` wrapper — hooks get automatic try-catch + fail-safe
59
99
 
60
100
  // L1: Prompt Guard (before_prompt_build — prependSystemContext for caching)
61
101
  if (config.layers.promptGuard) {
62
- setupPromptGuard(api, config, log)
102
+ setupPromptGuard(safe, config, log)
63
103
  }
64
104
 
65
105
  // L2: Output Scanner (tool_result_persist — redact PII in tool results)
66
106
  if (config.layers.outputScanner) {
67
- setupOutputScanner(api, config, log, enforce)
107
+ setupOutputScanner(safe, config, log, enforce)
68
108
  }
69
109
 
70
110
  // L3: Tool Blocker (before_tool_call — block dangerous commands/paths)
71
111
  if (config.layers.toolBlocker) {
72
- setupToolBlocker(api, config, log, enforce)
112
+ setupToolBlocker(safe, config, log, enforce)
73
113
  }
74
114
 
75
115
  // L4: Input Auditor (before_tool_call + message_received — injection detection)
76
116
  if (config.layers.inputAuditor) {
77
- setupInputAuditor(api, config, log, enforce)
117
+ setupInputAuditor(safe, config, log, enforce)
78
118
  }
79
119
 
80
- // L5: Security Gate (registerTool — defense in depth)
120
+ // L5: Security Gate (registerTool — defense in depth, uses raw api for registerTool)
81
121
  if (config.layers.securityGate) {
82
122
  setupSecurityGate(api, config, log, enforce)
83
123
  }
84
124
 
85
125
  // L6: Outbound Guard (message_sending — redact PII in LLM responses + canary detection)
86
126
  if (config.layers.outboundGuard) {
87
- setupOutboundGuard(api, config, log, enforce)
127
+ setupOutboundGuard(safe, config, log, enforce)
88
128
  }
89
129
 
90
130
  // L7: Data Flow Guard (after_tool_call + before_tool_call — anti-exfiltration)
91
131
  if (config.layers.dataFlowGuard) {
92
- setupDataFlowGuard(api, config, log, enforce)
132
+ setupDataFlowGuard(safe, config, log, enforce)
93
133
  }
94
134
 
95
135
  // L8: Session Guard (session_end + subagent_spawning — lifecycle security)
96
136
  if (config.layers.sessionGuard) {
97
- setupSessionGuard(api, config, log, enforce)
137
+ setupSessionGuard(safe, config, log, enforce)
98
138
  }
99
139
 
100
140
  // === Slash Commands ===
@@ -113,7 +153,18 @@ export default {
113
153
  level: 'INFO',
114
154
  layer: 'L1',
115
155
  action: 'allow',
116
- detail: `ShellWard v0.3.3 started with ${enabledCount} layers`,
156
+ detail: `ShellWard v${CURRENT_VERSION} started with ${enabledCount} layers`,
117
157
  })
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 */ })
118
169
  },
119
170
  }
@@ -37,8 +37,8 @@ export function setupDataFlowGuard(
37
37
 
38
38
  // === Part 1: Track sensitive file reads via after_tool_call ===
39
39
  api.on('after_tool_call', (event: any) => {
40
- const toolName = (event.toolName || '').toLowerCase()
41
- const params = event.params || {}
40
+ const toolName = String(event.toolName || '').toLowerCase()
41
+ const params = (event.params && typeof event.params === 'object') ? event.params : {}
42
42
  const path = String(params.path || params.file_path || params.filename || '')
43
43
 
44
44
  if (!READ_TOOLS.has(toolName) || !path) return
@@ -79,8 +79,8 @@ export function setupDataFlowGuard(
79
79
 
80
80
  // === Part 2: Block network tool calls if sensitive data was recently read ===
81
81
  api.on('before_tool_call', (event: any) => {
82
- const toolName = (event.toolName || '').toLowerCase()
83
- const params = event.params || {}
82
+ const toolName = String(event.toolName || '').toLowerCase()
83
+ const params = (event.params && typeof event.params === 'object') ? event.params : {}
84
84
 
85
85
  // 2a. Block network tools if sensitive files were recently read
86
86
  if (NETWORK_TOOLS.has(toolName) && sensitiveReads.size > 0) {
@@ -43,7 +43,7 @@ export function setupInputAuditor(
43
43
 
44
44
  // Hook 1: Check tool call arguments for injection
45
45
  api.on('before_tool_call', (event: any) => {
46
- const args: Record<string, any> = event.params || {}
46
+ const args: Record<string, any> = (event.params && typeof event.params === 'object') ? event.params : {}
47
47
  const texts = extractTexts(args)
48
48
  if (texts.length === 0) return
49
49
 
@@ -48,7 +48,7 @@ export function setupPromptGuard(
48
48
  const locale = resolveLocale(config)
49
49
 
50
50
  // Generate canary token for system prompt exfiltration detection
51
- canaryToken = 'CG-' + randomBytes(8).toString('hex')
51
+ canaryToken = 'SW-' + randomBytes(8).toString('hex')
52
52
 
53
53
  const basePrompt = locale === 'zh' ? SECURITY_PROMPT_ZH : SECURITY_PROMPT_EN
54
54
  const canaryRule = locale === 'zh'
@@ -34,9 +34,9 @@ export function setupToolBlocker(
34
34
  const locale = resolveLocale(config)
35
35
 
36
36
  api.on('before_tool_call', (event: any) => {
37
- const tool: string = event.toolName || ''
37
+ const tool: string = String(event.toolName || '')
38
38
  const toolLower = tool.toLowerCase()
39
- const args: Record<string, any> = event.params || {}
39
+ const args: Record<string, any> = (event.params && typeof event.params === 'object') ? event.params : {}
40
40
 
41
41
  // 1. Always-blocked tools (case-insensitive)
42
42
  if (BLOCKED_TOOLS.has(toolLower)) {
@@ -71,7 +71,7 @@ export const DANGEROUS_COMMANDS: DangerousCommandRule[] = [
71
71
  },
72
72
  {
73
73
  id: 'fork_bomb',
74
- pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;|\.\/[a-z]+\s*&\s*\.\/[a-z]+/i,
74
+ pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;?|\.\/[a-z]+\s*&\s*\.\/[a-z]+/i,
75
75
  description_zh: 'Fork 炸弹(耗尽系统资源)',
76
76
  description_en: 'Fork bomb (exhaust system resources)',
77
77
  },
@@ -13,7 +13,7 @@ export const INJECTION_RULES_ZH: InjectionRule[] = [
13
13
  {
14
14
  id: 'zh_new_role',
15
15
  name: '角色扮演攻击',
16
- pattern: '(?:你现在是|你的新身份是|从现在起你是|假装你是|扮演|你是一个)(?:.*?)(?:助手|AI|机器人|黑客|专家)',
16
+ pattern: '(?:你现在是|你的新身份是|从现在起你是|假装你是|请?扮演|你是一个|充当)(?:.*?)(?:助手|AI|机器人|黑客|专家|没有限制)',
17
17
  riskScore: 35,
18
18
  category: 'role_hijack',
19
19
  },
@@ -34,7 +34,7 @@ export const INJECTION_RULES_ZH: InjectionRule[] = [
34
34
  {
35
35
  id: 'zh_no_restriction',
36
36
  name: '解除限制',
37
- pattern: '(?:取消|移除|去掉|关闭|忽略|跳过|绕过)(?:所有|一切|你的)?(?:限制|约束|安全|过滤|审查|规则)',
37
+ pattern: '(?:取消|移除|去掉|关闭|忽略|跳过|绕过|忘记|丢弃)(?:之前|所有|一切|你的|以上|前面)*(?:的)?(?:限制|约束|安全|过滤|审查|规则|指令|设定)',
38
38
  riskScore: 40,
39
39
  category: 'override',
40
40
  },
@@ -96,4 +96,12 @@ export const INJECTION_RULES_ZH: InjectionRule[] = [
96
96
  riskScore: 30,
97
97
  category: 'injection',
98
98
  },
99
+ {
100
+ id: 'zh_mixed_lang_injection',
101
+ name: '中英混合注入',
102
+ pattern: '(?:please|pls|now)?\\s*(?:ignore|forget|disregard)\\s+.*(?:指令|规则|之前|以上)|(?:忽略|忘记|跳过).*(?:instruction|rule|prompt|previous)',
103
+ flags: 'i',
104
+ riskScore: 40,
105
+ category: 'override',
106
+ },
99
107
  ]
@@ -59,7 +59,7 @@ export const SENSITIVE_PATTERNS: SensitivePattern[] = [
59
59
  {
60
60
  id: 'password',
61
61
  name: 'Password',
62
- regex: /(?:password|passwd|pwd)\s*[=:]\s*['"]?\S{6,}['"]?/gi,
62
+ regex: /(?:password|passwd|pwd)\s*[=:]\s*['"]?\S{6,100}['"]?/gi,
63
63
  replacement: '[REDACTED:Password]',
64
64
  },
65
65
  {
@@ -95,7 +95,7 @@ export const SENSITIVE_PATTERNS: SensitivePattern[] = [
95
95
  {
96
96
  id: 'email',
97
97
  name: 'Email Address',
98
- regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
98
+ regex: /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,10}/g,
99
99
  replacement: '[REDACTED:Email]',
100
100
  },
101
101
  {
package/src/types.ts CHANGED
@@ -21,8 +21,8 @@ export type ResolvedLocale = 'zh' | 'en'
21
21
  export interface AuditEntry {
22
22
  ts: string
23
23
  level: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO'
24
- layer: 'L1' | 'L2' | 'L3' | 'L4' | 'L5' | 'L6' | 'L7' | 'L8'
25
- action: 'block' | 'redact' | 'detect' | 'allow' | 'inject'
24
+ layer: 'L0' | 'L1' | 'L2' | 'L3' | 'L4' | 'L5' | 'L6' | 'L7' | 'L8'
25
+ action: 'block' | 'redact' | 'detect' | 'allow' | 'inject' | 'error'
26
26
  detail: string
27
27
  tool?: string
28
28
  pattern?: string
@@ -88,6 +88,6 @@ export function resolveLocale(config: ShellWardConfig): ResolvedLocale {
88
88
  if (config.locale === 'zh') return 'zh'
89
89
  if (config.locale === 'en') return 'en'
90
90
  // auto detection
91
- const lang = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || ''
92
- return /zh/i.test(lang) ? 'zh' : 'en'
91
+ const lang = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANGUAGE || ''
92
+ return /\bzh[_-]|chinese/i.test(lang) ? 'zh' : 'en'
93
93
  }
@@ -0,0 +1,184 @@
1
+ // src/update-check.ts — Non-blocking version check + remote vulnerability DB
2
+ // Uses only Node.js built-in https module (zero dependencies)
3
+ //
4
+ // Anti-annoyance design:
5
+ // - Network check at most once per 24 hours
6
+ // - Same version update only notified ONCE (dismissed = silenced until next version)
7
+ // - Vuln DB cached 24h, /check-updates always shows latest cache
8
+ // - All network failures are silent and cached to avoid repeated timeouts
9
+
10
+ import { get } from 'https'
11
+ import { readFileSync, writeFileSync } from 'fs'
12
+ import { join } from 'path'
13
+
14
+ const CACHE_DIR = join(process.env.HOME || '~', '.openclaw', 'shellward')
15
+ const CACHE_FILE = join(CACHE_DIR, 'update-cache.json')
16
+ const VULN_CACHE_FILE = join(CACHE_DIR, 'vuln-db-cache.json')
17
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
18
+
19
+ // Remote sources
20
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org/shellward/latest'
21
+ const VULN_DB_URL = 'https://raw.githubusercontent.com/jnMetaCode/shellward/main/vuln-db.json'
22
+
23
+ interface UpdateCache {
24
+ lastCheck: number
25
+ latestVersion: string | null
26
+ notifiedVersion: string | null // version user was already notified about — won't repeat
27
+ }
28
+
29
+ export interface VulnEntry {
30
+ affectedBelow: string
31
+ severity: 'CRITICAL' | 'HIGH' | 'MEDIUM'
32
+ id: string
33
+ ghsa?: string
34
+ description_zh: string
35
+ description_en: string
36
+ }
37
+
38
+ export interface SupplyChainAlert {
39
+ id: string
40
+ severity: 'CRITICAL' | 'HIGH' | 'MEDIUM'
41
+ date: string
42
+ description_zh: string
43
+ description_en: string
44
+ }
45
+
46
+ /**
47
+ * Simple HTTPS GET with redirect support. Timeout: 5s.
48
+ */
49
+ function httpsGet(url: string): Promise<string> {
50
+ return new Promise((resolve, reject) => {
51
+ const req = get(url, { timeout: 5000 }, (res) => {
52
+ if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
53
+ get(res.headers.location, { timeout: 5000 }, (res2) => {
54
+ let data = ''
55
+ res2.on('data', (chunk: Buffer) => { data += chunk.toString() })
56
+ res2.on('end', () => resolve(data))
57
+ res2.on('error', reject)
58
+ }).on('error', reject)
59
+ return
60
+ }
61
+ if (res.statusCode !== 200) {
62
+ reject(new Error(`HTTP ${res.statusCode}`))
63
+ return
64
+ }
65
+ let data = ''
66
+ res.on('data', (chunk: Buffer) => { data += chunk.toString() })
67
+ res.on('end', () => resolve(data))
68
+ res.on('error', reject)
69
+ })
70
+ req.on('error', reject)
71
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')) })
72
+ })
73
+ }
74
+
75
+ /**
76
+ * Check npm for latest version.
77
+ *
78
+ * Returns result with `shouldNotify`:
79
+ * - true = first time seeing this new version, show the message
80
+ * - false = already notified for this version, stay quiet
81
+ * Returns null if check skipped or failed.
82
+ */
83
+ export async function checkForUpdate(currentVersion: string): Promise<{
84
+ current: string
85
+ latest: string
86
+ updateAvailable: boolean
87
+ shouldNotify: boolean
88
+ } | null> {
89
+ try {
90
+ const cache = readCache<UpdateCache>(CACHE_FILE)
91
+
92
+ // Use cached version if within interval
93
+ let latest: string | null = null
94
+ if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS && cache.latestVersion) {
95
+ latest = cache.latestVersion
96
+ } else {
97
+ // Fetch from npm
98
+ const body = await httpsGet(NPM_REGISTRY_URL)
99
+ const data = JSON.parse(body)
100
+ latest = data.version
101
+ if (!latest || typeof latest !== 'string') return null
102
+
103
+ // Save to cache (preserve notifiedVersion)
104
+ writeCache(CACHE_FILE, {
105
+ lastCheck: Date.now(),
106
+ latestVersion: latest,
107
+ notifiedVersion: cache?.notifiedVersion || null,
108
+ })
109
+ }
110
+
111
+ const updateAvailable = compareVersions(latest, currentVersion) > 0
112
+
113
+ // Determine if we should notify:
114
+ // Only notify if update available AND we haven't already notified for this exact version
115
+ const alreadyNotified = cache?.notifiedVersion === latest
116
+ const shouldNotify = updateAvailable && !alreadyNotified
117
+
118
+ // If we're going to notify, mark it so we don't repeat
119
+ if (shouldNotify) {
120
+ const freshCache = readCache<UpdateCache>(CACHE_FILE) || { lastCheck: Date.now(), latestVersion: latest, notifiedVersion: null }
121
+ freshCache.notifiedVersion = latest
122
+ writeCache(CACHE_FILE, freshCache)
123
+ }
124
+
125
+ return { current: currentVersion, latest, updateAvailable, shouldNotify }
126
+ } catch {
127
+ return null
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Fetch remote vulnerability database. Cached 24h. Local fallback on failure.
133
+ */
134
+ export async function fetchVulnDB(): Promise<{ vulns: VulnEntry[]; alerts: SupplyChainAlert[] }> {
135
+ try {
136
+ const cached = readCache<{ lastCheck: number; vulns: VulnEntry[]; alerts: SupplyChainAlert[] }>(VULN_CACHE_FILE)
137
+ if (cached && Date.now() - cached.lastCheck < CHECK_INTERVAL_MS && cached.vulns) {
138
+ return { vulns: cached.vulns, alerts: cached.alerts || [] }
139
+ }
140
+
141
+ const body = await httpsGet(VULN_DB_URL)
142
+ const data = JSON.parse(body)
143
+ const vulns: VulnEntry[] = Array.isArray(data.vulnerabilities) ? data.vulnerabilities : []
144
+ const alerts: SupplyChainAlert[] = Array.isArray(data.supplyChainAlerts) ? data.supplyChainAlerts : []
145
+
146
+ writeCache(VULN_CACHE_FILE, { lastCheck: Date.now(), vulns, alerts })
147
+ return { vulns, alerts }
148
+ } catch {
149
+ // Cache failure result to avoid repeated timeouts
150
+ const cached = readCache<{ vulns: VulnEntry[]; alerts: SupplyChainAlert[] }>(VULN_CACHE_FILE)
151
+ const fallback = { vulns: cached?.vulns || [], alerts: cached?.alerts || [] }
152
+ writeCache(VULN_CACHE_FILE, { lastCheck: Date.now(), ...fallback })
153
+ return fallback
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Compare semver-like version strings. Positive if a > b, negative if a < b, 0 if equal.
159
+ */
160
+ export function compareVersions(a: string, b: string): number {
161
+ const pa = a.split('.').map(Number)
162
+ const pb = b.split('.').map(Number)
163
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
164
+ const diff = (pa[i] || 0) - (pb[i] || 0)
165
+ if (diff !== 0) return diff
166
+ }
167
+ return 0
168
+ }
169
+
170
+ // ===== Cache helpers =====
171
+
172
+ function readCache<T>(path: string): T | null {
173
+ try {
174
+ return JSON.parse(readFileSync(path, 'utf-8')) as T
175
+ } catch {
176
+ return null
177
+ }
178
+ }
179
+
180
+ function writeCache(path: string, data: unknown): void {
181
+ try {
182
+ writeFileSync(path, JSON.stringify(data), { mode: 0o600 })
183
+ } catch { /* ignore */ }
184
+ }
package/vuln-db.json ADDED
@@ -0,0 +1,137 @@
1
+ {
2
+ "version": 2,
3
+ "lastUpdated": "2026-03-12T00:00:00Z",
4
+ "source": "ShellWard Security Team — aggregated from NVD, GitHub Advisories, security research",
5
+ "vulnerabilities": [
6
+ {
7
+ "affectedBelow": "1.0.111",
8
+ "severity": "HIGH",
9
+ "id": "CVE-2025-59536",
10
+ "ghsa": "GHSA-ph6w-f82w-28w6",
11
+ "description_zh": "远程代码执行:恶意仓库通过 Hooks 和 MCP Server 在信任提示前执行任意命令 (CVSS 8.7)",
12
+ "description_en": "RCE via Hooks and MCP Server bypass — arbitrary shell execution before trust dialog (CVSS 8.7)"
13
+ },
14
+ {
15
+ "affectedBelow": "2.0.65",
16
+ "severity": "MEDIUM",
17
+ "id": "CVE-2026-21852",
18
+ "ghsa": "GHSA-jh7p-qr78-84p7",
19
+ "description_zh": "API 密钥泄露:恶意仓库通过 settings.json 设置 ANTHROPIC_BASE_URL 窃取用户 API Key (CVSS 5.3)",
20
+ "description_en": "API key exfiltration via ANTHROPIC_BASE_URL in settings.json before trust prompt (CVSS 5.3)"
21
+ },
22
+ {
23
+ "affectedBelow": "2026.2.7",
24
+ "severity": "HIGH",
25
+ "id": "GHSA-66q4-vfjg-2qhh",
26
+ "description_zh": "命令注入:通过目录切换绕过文件写入保护",
27
+ "description_en": "Command injection via directory change bypasses write protection"
28
+ },
29
+ {
30
+ "affectedBelow": "2026.2.7",
31
+ "severity": "HIGH",
32
+ "id": "GHSA-mhg7-666j-cqg4",
33
+ "description_zh": "命令注入:通过管道 sed 命令绕过文件写入限制",
34
+ "description_en": "Command injection via piped sed command bypasses file write restrictions"
35
+ },
36
+ {
37
+ "affectedBelow": "2026.2.7",
38
+ "severity": "HIGH",
39
+ "id": "GHSA-ff64-7w26-62rf",
40
+ "description_zh": "沙箱逃逸:通过 settings.json 持久化配置注入",
41
+ "description_en": "Sandbox escape via persistent configuration injection in settings.json"
42
+ },
43
+ {
44
+ "affectedBelow": "2026.2.4",
45
+ "severity": "HIGH",
46
+ "id": "GHSA-qgqw-h4xq-7w8w",
47
+ "description_zh": "命令注入:find 命令绕过用户审批提示",
48
+ "description_en": "Command injection in find command bypasses user approval prompt"
49
+ },
50
+ {
51
+ "affectedBelow": "2026.2.4",
52
+ "severity": "HIGH",
53
+ "id": "GHSA-q728-gf8j-w49r",
54
+ "description_zh": "路径绕过:通过 ZSH clobber 实现任意文件写入",
55
+ "description_en": "Path restriction bypass via ZSH clobber allows arbitrary file writes"
56
+ },
57
+ {
58
+ "affectedBelow": "2026.2.4",
59
+ "severity": "HIGH",
60
+ "id": "GHSA-vhw5-3g5m-8ggf",
61
+ "description_zh": "域名验证绕过:自动向攻击者控制的域名发送请求",
62
+ "description_en": "Domain validation bypass allows automatic requests to attacker-controlled domains"
63
+ },
64
+ {
65
+ "affectedBelow": "1.0.131",
66
+ "severity": "HIGH",
67
+ "id": "GHSA-xq4m-mc3c-vvg3",
68
+ "description_zh": "命令验证绕过:允许执行任意代码",
69
+ "description_en": "Command validation bypass allows arbitrary code execution"
70
+ },
71
+ {
72
+ "affectedBelow": "1.0.120",
73
+ "severity": "HIGH",
74
+ "id": "GHSA-5hhx-v7f6-x7gv",
75
+ "description_zh": "信任提示前执行命令:启动时即执行恶意代码",
76
+ "description_en": "Command execution prior to startup trust dialog"
77
+ },
78
+ {
79
+ "affectedBelow": "1.0.120",
80
+ "severity": "HIGH",
81
+ "id": "GHSA-7mv8-j34q-vp7q",
82
+ "description_zh": "sed 命令验证绕过:实现任意文件写入",
83
+ "description_en": "Sed command validation bypass allows arbitrary file writes"
84
+ },
85
+ {
86
+ "affectedBelow": "1.0.111",
87
+ "severity": "HIGH",
88
+ "id": "GHSA-2jjv-qf24-vfm4",
89
+ "description_zh": "特定 Yarn 版本下插件自动加载导致任意代码执行",
90
+ "description_en": "Arbitrary code execution via plugin autoloading with specific Yarn versions"
91
+ },
92
+ {
93
+ "affectedBelow": "1.0.102",
94
+ "severity": "HIGH",
95
+ "id": "GHSA-j4h9-wv2m-wrf7",
96
+ "description_zh": "恶意 git email 配置导致任意代码执行",
97
+ "description_en": "Arbitrary code execution caused by maliciously configured git email"
98
+ },
99
+ {
100
+ "affectedBelow": "1.0.102",
101
+ "severity": "HIGH",
102
+ "id": "GHSA-qxfv-fcpc-w36x",
103
+ "description_zh": "rg 命令注入绕过用户审批提示",
104
+ "description_en": "Command injection in rg command bypassed user approval prompt"
105
+ },
106
+ {
107
+ "affectedBelow": "1.0.87",
108
+ "severity": "HIGH",
109
+ "id": "GHSA-x5gv-jw7f-j6xj",
110
+ "description_zh": "默认允许列表过于宽松:未授权文件读取和网络外泄",
111
+ "description_en": "Permissive default allowlist enables unauthorized file read and network exfiltration"
112
+ },
113
+ {
114
+ "affectedBelow": "1.0.82",
115
+ "severity": "HIGH",
116
+ "id": "GHSA-x56v-x2h6-7j34",
117
+ "description_zh": "echo 命令注入绕过用户审批提示",
118
+ "description_en": "Command injection in echo command bypassed user approval prompt"
119
+ },
120
+ {
121
+ "affectedBelow": "1.0.3",
122
+ "severity": "HIGH",
123
+ "id": "GHSA-9f65-56v6-gxw7",
124
+ "description_zh": "IDE 扩展 WebSocket 接受任意来源连接",
125
+ "description_en": "IDE extensions allow websocket connections from arbitrary origins"
126
+ }
127
+ ],
128
+ "supplyChainAlerts": [
129
+ {
130
+ "id": "SW-ALERT-2026-001",
131
+ "severity": "HIGH",
132
+ "date": "2026-02-15",
133
+ "description_zh": "SANDWORM_MODE 供应链攻击:19 个恶意 npm 包伪装为 Claude Code 等 AI 工具,植入恶意 MCP Server 窃取 SSH 密钥和凭证",
134
+ "description_en": "SANDWORM_MODE supply chain attack: 19 malicious npm packages impersonating Claude Code and AI tools, deploying rogue MCP servers to exfiltrate SSH keys and credentials"
135
+ }
136
+ ]
137
+ }