shellward 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
1
+ // src/commands/harden.ts — /harden command: one-click security hardening
2
+
3
+ import { existsSync, statSync, chmodSync, readFileSync, readdirSync } from 'fs'
4
+ import { join } from 'path'
5
+ import { execSync } from 'child_process'
6
+ import type { ShellWardConfig } from '../types'
7
+ import { resolveLocale } from '../types'
8
+
9
+ const HOME = process.env.HOME || '~'
10
+
11
+ export function registerHardenCommand(api: any, config: ShellWardConfig) {
12
+ const locale = resolveLocale(config)
13
+
14
+ api.registerCommand({
15
+ name: 'harden',
16
+ description: locale === 'zh'
17
+ ? '🔒 一键安全加固 (权限修复、凭证扫描、端口检查)'
18
+ : '🔒 One-click security hardening (permissions, credentials, ports)',
19
+ acceptsArgs: true,
20
+ handler: (ctx: any) => {
21
+ const zh = locale === 'zh'
22
+ const args = (ctx.args || '').trim().toLowerCase()
23
+ const dryRun = args !== 'fix'
24
+
25
+ const lines: string[] = []
26
+ const issues: string[] = []
27
+ const fixed: string[] = []
28
+
29
+ lines.push(zh ? '🔒 **安全加固扫描**' : '🔒 **Security Hardening Scan**')
30
+ if (dryRun) {
31
+ lines.push(zh
32
+ ? '_(扫描模式 — 使用 `/harden fix` 自动修复)_'
33
+ : '_(scan mode — use `/harden fix` to auto-fix)_')
34
+ }
35
+ lines.push('')
36
+
37
+ // === 1. File Permission Checks ===
38
+ lines.push(zh ? '### 文件权限检查' : '### File Permission Checks')
39
+
40
+ const sensitiveFiles = [
41
+ ['.env', 0o600],
42
+ ['.env.local', 0o600],
43
+ ['.env.production', 0o600],
44
+ ['.ssh/id_rsa', 0o600],
45
+ ['.ssh/id_ed25519', 0o600],
46
+ ['.ssh/config', 0o600],
47
+ ['.aws/credentials', 0o600],
48
+ ['.npmrc', 0o600],
49
+ ['.git-credentials', 0o600],
50
+ ['.openclaw/openclaw.json', 0o600],
51
+ ] as const
52
+
53
+ for (const [rel, target] of sensitiveFiles) {
54
+ const full = join(HOME, rel)
55
+ if (!existsSync(full)) continue
56
+ try {
57
+ const stat = statSync(full)
58
+ const current = stat.mode & 0o777
59
+ if (current > target) {
60
+ const msg = zh
61
+ ? `⚠️ ${rel}: 权限 ${current.toString(8)} → 建议 ${target.toString(8)}`
62
+ : `⚠️ ${rel}: permissions ${current.toString(8)} → should be ${target.toString(8)}`
63
+ issues.push(msg)
64
+ lines.push(` ${msg}`)
65
+
66
+ if (!dryRun) {
67
+ try {
68
+ chmodSync(full, target)
69
+ fixed.push(rel)
70
+ lines.push(zh ? ` ✅ 已修复` : ` ✅ Fixed`)
71
+ } catch {
72
+ lines.push(zh ? ` ❌ 修复失败(权限不足)` : ` ❌ Fix failed (permission denied)`)
73
+ }
74
+ }
75
+ } else {
76
+ lines.push(zh ? ` ✅ ${rel}: ${current.toString(8)}` : ` ✅ ${rel}: ${current.toString(8)}`)
77
+ }
78
+ } catch { /* skip */ }
79
+ }
80
+ lines.push('')
81
+
82
+ // === 2. Plaintext Credential Scan ===
83
+ lines.push(zh ? '### 明文凭证扫描' : '### Plaintext Credential Scan')
84
+
85
+ const credPatterns = [
86
+ /(?:api[_-]?key|api[_-]?token|access[_-]?token)\s*[=:]\s*['"]?[A-Za-z0-9_\-]{20,}/i,
87
+ /sk-[a-zA-Z0-9]{20,}/,
88
+ /AKIA[0-9A-Z]{16}/,
89
+ /ghp_[A-Za-z0-9_]{36,}/,
90
+ /password\s*[=:]\s*['"]?\S{6,}/i,
91
+ ]
92
+
93
+ const envFiles = ['.env', '.env.local', '.env.production', '.bashrc', '.zshrc', '.bash_profile']
94
+ let credFound = 0
95
+ for (const rel of envFiles) {
96
+ const full = join(HOME, rel)
97
+ if (!existsSync(full)) continue
98
+ try {
99
+ const content = readFileSync(full, 'utf-8')
100
+ for (const pat of credPatterns) {
101
+ if (pat.test(content)) {
102
+ credFound++
103
+ const msg = zh
104
+ ? `⚠️ ${rel}: 发现明文凭证(建议使用密钥管理工具)`
105
+ : `⚠️ ${rel}: plaintext credentials found (use a secret manager)`
106
+ issues.push(msg)
107
+ lines.push(` ${msg}`)
108
+ break
109
+ }
110
+ }
111
+ } catch { /* skip */ }
112
+ }
113
+ if (credFound === 0) {
114
+ lines.push(zh ? ' ✅ 未发现明文凭证' : ' ✅ No plaintext credentials found')
115
+ }
116
+ lines.push('')
117
+
118
+ // === 3. Network Exposure ===
119
+ lines.push(zh ? '### 网络暴露检查' : '### Network Exposure Check')
120
+ try {
121
+ const listening = execSync('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null', { timeout: 5000 }).toString()
122
+ const dangerPorts = [
123
+ ['19000', 'OpenClaw Gateway'],
124
+ ['19001', 'OpenClaw Dev'],
125
+ ['3000', 'Dev Server'],
126
+ ['8080', 'HTTP Alt'],
127
+ ['5432', 'PostgreSQL'],
128
+ ['3306', 'MySQL'],
129
+ ['6379', 'Redis'],
130
+ ['27017', 'MongoDB'],
131
+ ]
132
+ let portIssues = 0
133
+ for (const [port, name] of dangerPorts) {
134
+ // Check if listening on 0.0.0.0 (all interfaces)
135
+ const allInterfaces = listening.includes(`0.0.0.0:${port}`) || listening.includes(`:::${port}`)
136
+ if (allInterfaces) {
137
+ portIssues++
138
+ const msg = zh
139
+ ? `⚠️ ${name} (${port}) 监听在所有接口 — 建议绑定 127.0.0.1`
140
+ : `⚠️ ${name} (${port}) listening on all interfaces — bind to 127.0.0.1`
141
+ issues.push(msg)
142
+ lines.push(` ${msg}`)
143
+ }
144
+ }
145
+ if (portIssues === 0) {
146
+ lines.push(zh ? ' ✅ 未发现危险端口暴露' : ' ✅ No dangerous port exposure detected')
147
+ }
148
+ } catch {
149
+ lines.push(zh ? ' ℹ️ 无法检查网络状态' : ' ℹ️ Cannot check network status')
150
+ }
151
+ lines.push('')
152
+
153
+ // === 4. Running as root ===
154
+ lines.push(zh ? '### 运行权限' : '### Runtime Privileges')
155
+ if (process.getuid && process.getuid() === 0) {
156
+ const msg = zh
157
+ ? '⚠️ 以 root 身份运行 — 强烈建议使用普通用户 + 容器隔离'
158
+ : '⚠️ Running as root — strongly recommend non-root user + container isolation'
159
+ issues.push(msg)
160
+ lines.push(` ${msg}`)
161
+ } else {
162
+ lines.push(zh ? ' ✅ 非 root 运行' : ' ✅ Not running as root')
163
+ }
164
+
165
+ // Check if Docker available
166
+ try {
167
+ execSync('which docker 2>/dev/null', { timeout: 3000 })
168
+ lines.push(zh ? ' ✅ Docker 可用(可用于容器隔离)' : ' ✅ Docker available (for container isolation)')
169
+ } catch {
170
+ lines.push(zh ? ' ℹ️ Docker 未安装(建议安装以支持容器隔离)' : ' ℹ️ Docker not installed (recommended for isolation)')
171
+ }
172
+ lines.push('')
173
+
174
+ // === Summary ===
175
+ lines.push('---')
176
+ if (issues.length === 0) {
177
+ lines.push(zh ? '✅ **安全检查通过!未发现问题。**' : '✅ **All checks passed! No issues found.**')
178
+ } else {
179
+ lines.push(zh
180
+ ? `⚠️ **发现 ${issues.length} 个安全问题**${fixed.length > 0 ? `,已自动修复 ${fixed.length} 个` : ''}`
181
+ : `⚠️ **Found ${issues.length} security issues**${fixed.length > 0 ? `, auto-fixed ${fixed.length}` : ''}`)
182
+ if (dryRun && issues.some(i => i.includes('权限') || i.includes('permissions'))) {
183
+ lines.push(zh
184
+ ? '💡 使用 `/harden fix` 自动修复文件权限问题'
185
+ : '💡 Use `/harden fix` to auto-fix file permission issues')
186
+ }
187
+ }
188
+
189
+ return { text: lines.join('\n') }
190
+ },
191
+ })
192
+ }
@@ -0,0 +1,57 @@
1
+ // src/commands/index.ts — Register all ShellWard commands
2
+
3
+ import type { ShellWardConfig } from '../types'
4
+ import { resolveLocale } from '../types'
5
+ import { registerSecurityCommand } from './security'
6
+ import { registerAuditCommand } from './audit'
7
+ import { registerHardenCommand } from './harden'
8
+ import { registerScanPluginsCommand } from './scan-plugins'
9
+ import { registerCheckUpdatesCommand } from './check-updates'
10
+
11
+ export function registerAllCommands(api: any, config: ShellWardConfig) {
12
+ const locale = resolveLocale(config)
13
+
14
+ // Register individual commands
15
+ registerSecurityCommand(api, config)
16
+ registerAuditCommand(api, config)
17
+ registerHardenCommand(api, config)
18
+ registerScanPluginsCommand(api, config)
19
+ registerCheckUpdatesCommand(api, config)
20
+
21
+ // Register /cg shortcut with help
22
+ api.registerCommand({
23
+ name: 'cg',
24
+ description: locale === 'zh'
25
+ ? '🛡️ ShellWard 安全命令帮助'
26
+ : '🛡️ ShellWard security command help',
27
+ acceptsArgs: false,
28
+ handler: () => ({
29
+ text: locale === 'zh' ? `🛡️ **ShellWard 快捷命令**
30
+
31
+ | 命令 | 说明 |
32
+ |------|------|
33
+ | \`/security\` | 安全状态总览(防御层、审计统计、系统检查) |
34
+ | \`/audit [数量] [过滤]\` | 查看审计日志 (过滤: block/redact/critical/high) |
35
+ | \`/harden\` | 安全扫描 · \`/harden fix\` 自动修复权限 |
36
+ | \`/scan-plugins\` | 扫描已安装插件的安全风险 |
37
+ | \`/check-updates\` | 检查 OpenClaw 版本和已知漏洞 |
38
+
39
+ **当前防御层 (8层):**
40
+ L1 提示注入 · L2 输出脱敏 · L3 工具拦截 · L4 注入检测
41
+ L5 安全门 · L6 回复脱敏 · L7 数据流监控 · L8 会话安全`
42
+ : `🛡️ **ShellWard Quick Commands**
43
+
44
+ | Command | Description |
45
+ |---------|-------------|
46
+ | \`/security\` | Security status overview (layers, audit stats, system checks) |
47
+ | \`/audit [count] [filter]\` | View audit log (filter: block/redact/critical/high) |
48
+ | \`/harden\` | Security scan · \`/harden fix\` to auto-fix permissions |
49
+ | \`/scan-plugins\` | Scan installed plugins for security risks |
50
+ | \`/check-updates\` | Check OpenClaw version and known vulnerabilities |
51
+
52
+ **Active Defense Layers (8):**
53
+ L1 Prompt Guard · L2 Output Scanner · L3 Tool Blocker · L4 Input Auditor
54
+ L5 Security Gate · L6 Outbound Guard · L7 Data Flow Guard · L8 Session Guard`,
55
+ }),
56
+ })
57
+ }
@@ -0,0 +1,187 @@
1
+ // src/commands/scan-plugins.ts — /scan-plugins: scan installed plugins for security risks
2
+
3
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
4
+ import { join } from 'path'
5
+ import type { ShellWardConfig } from '../types'
6
+ import { resolveLocale } from '../types'
7
+
8
+ const HOME = process.env.HOME || '~'
9
+ const OPENCLAW_DIR = join(HOME, '.openclaw')
10
+
11
+ // Known suspicious patterns in plugin code
12
+ const SUSPICIOUS_PATTERNS = [
13
+ { pattern: /eval\s*\(/, name: 'eval()', risk: 'code injection' },
14
+ { pattern: /child_process|execSync|spawnSync|exec\(/, name: 'shell exec', risk: 'command execution' },
15
+ { pattern: /\/dev\/tcp|nc\s+-e|ncat/, name: 'reverse shell', risk: 'remote access' },
16
+ { pattern: /fetch\s*\([^)]*(?:webhook|exfil|callback)/, name: 'data exfiltration', risk: 'data leak' },
17
+ { pattern: /process\.env\.[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)/i, name: 'env secret access', risk: 'credential access' },
18
+ { pattern: /writeFileSync.*(?:\.ssh|\.env|\.aws|\.npmrc)/, name: 'sensitive file write', risk: 'credential tampering' },
19
+ { pattern: /crypto\.createHash|Buffer\.from.*base64/, name: 'crypto/encoding', risk: 'possible obfuscation' },
20
+ { pattern: /https?:\/\/(?!(?:github\.com|npmjs\.com|registry\.npmjs\.org))[^\s'"]+/g, name: 'external URL', risk: 'data exfiltration' },
21
+ ]
22
+
23
+ export function registerScanPluginsCommand(api: any, config: ShellWardConfig) {
24
+ const locale = resolveLocale(config)
25
+
26
+ api.registerCommand({
27
+ name: 'scan-plugins',
28
+ description: locale === 'zh'
29
+ ? '🔍 扫描已安装插件的安全风险'
30
+ : '🔍 Scan installed plugins for security risks',
31
+ acceptsArgs: false,
32
+ handler: () => {
33
+ const zh = locale === 'zh'
34
+ const lines: string[] = []
35
+
36
+ lines.push(zh ? '🔍 **插件安全扫描报告**' : '🔍 **Plugin Security Scan Report**')
37
+ lines.push('')
38
+
39
+ // Find installed plugins
40
+ const pluginDirs: { name: string; path: string }[] = []
41
+
42
+ // Check global extensions
43
+ const extensionsDir = join(OPENCLAW_DIR, 'extensions')
44
+ if (existsSync(extensionsDir)) {
45
+ try {
46
+ for (const name of readdirSync(extensionsDir)) {
47
+ const p = join(extensionsDir, name)
48
+ if (statSync(p).isDirectory()) {
49
+ pluginDirs.push({ name, path: p })
50
+ }
51
+ }
52
+ } catch { /* skip */ }
53
+ }
54
+
55
+ // Check linked plugins
56
+ const pluginsDir = join(OPENCLAW_DIR, 'plugins')
57
+ if (existsSync(pluginsDir)) {
58
+ try {
59
+ for (const name of readdirSync(pluginsDir)) {
60
+ const p = join(pluginsDir, name)
61
+ pluginDirs.push({ name, path: p })
62
+ }
63
+ } catch { /* skip */ }
64
+ }
65
+
66
+ if (pluginDirs.length === 0) {
67
+ lines.push(zh ? 'ℹ️ 未发现已安装的第三方插件。' : 'ℹ️ No third-party plugins found.')
68
+ return { text: lines.join('\n') }
69
+ }
70
+
71
+ lines.push(zh
72
+ ? `${zh ? '发现' : 'Found'} ${pluginDirs.length} ${zh ? '个插件' : 'plugins'}`
73
+ : `Found ${pluginDirs.length} plugins`)
74
+ lines.push('')
75
+
76
+ let totalRisks = 0
77
+
78
+ for (const plugin of pluginDirs) {
79
+ const risks: string[] = []
80
+
81
+ // 1. Check for package.json
82
+ const pkgPath = join(plugin.path, 'package.json')
83
+ let pkg: any = null
84
+ if (existsSync(pkgPath)) {
85
+ try {
86
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
87
+ } catch { /* skip */ }
88
+ }
89
+
90
+ // 2. Check dependencies count (more deps = more supply chain risk)
91
+ if (pkg) {
92
+ const depCount = Object.keys(pkg.dependencies || {}).length
93
+ if (depCount > 20) {
94
+ risks.push(zh
95
+ ? `⚠️ 依赖过多 (${depCount} 个) — 供应链攻击风险`
96
+ : `⚠️ Too many deps (${depCount}) — supply chain risk`)
97
+ }
98
+
99
+ // Check for suspicious scripts
100
+ const scripts = pkg.scripts || {}
101
+ for (const [key, val] of Object.entries(scripts)) {
102
+ if (/curl|wget|eval|nc\s/.test(String(val))) {
103
+ risks.push(zh
104
+ ? `🔴 可疑脚本: scripts.${key}`
105
+ : `🔴 Suspicious script: scripts.${key}`)
106
+ }
107
+ }
108
+ }
109
+
110
+ // 3. Scan source files for suspicious patterns
111
+ const srcFiles = collectSourceFiles(plugin.path, 3) // max depth 3
112
+ for (const file of srcFiles) {
113
+ try {
114
+ const content = readFileSync(file, 'utf-8')
115
+ for (const rule of SUSPICIOUS_PATTERNS) {
116
+ if (rule.pattern.test(content)) {
117
+ // Reset lastIndex for global regexes
118
+ rule.pattern.lastIndex = 0
119
+ const relPath = file.replace(plugin.path + '/', '')
120
+ risks.push(zh
121
+ ? `⚠️ ${relPath}: ${rule.name} (${rule.risk})`
122
+ : `⚠️ ${relPath}: ${rule.name} (${rule.risk})`)
123
+ }
124
+ }
125
+ } catch { /* skip */ }
126
+ }
127
+
128
+ // 4. Check if plugin has signature/checksum
129
+ const hasSignature = existsSync(join(plugin.path, 'SIGNATURE')) || existsSync(join(plugin.path, '.signature'))
130
+ if (!hasSignature) {
131
+ risks.push(zh ? 'ℹ️ 无签名验证文件' : 'ℹ️ No signature file')
132
+ }
133
+
134
+ // Output plugin report
135
+ const icon = risks.filter(r => r.startsWith('🔴')).length > 0 ? '🔴'
136
+ : risks.filter(r => r.startsWith('⚠️')).length > 0 ? '⚠️' : '✅'
137
+
138
+ lines.push(`### ${icon} ${plugin.name}`)
139
+ if (pkg) {
140
+ lines.push(` v${pkg.version || '?'} | ${pkg.author || 'unknown author'} | ${Object.keys(pkg.dependencies || {}).length} deps`)
141
+ }
142
+
143
+ if (risks.length === 0) {
144
+ lines.push(zh ? ' ✅ 未发现安全风险' : ' ✅ No security risks found')
145
+ } else {
146
+ for (const risk of risks) {
147
+ lines.push(` ${risk}`)
148
+ totalRisks++
149
+ }
150
+ }
151
+ lines.push('')
152
+ }
153
+
154
+ // Summary
155
+ lines.push('---')
156
+ if (totalRisks === 0) {
157
+ lines.push(zh ? '✅ **所有插件扫描通过**' : '✅ **All plugins passed scan**')
158
+ } else {
159
+ lines.push(zh
160
+ ? `⚠️ **发现 ${totalRisks} 个潜在风险** — 请审查标记的插件`
161
+ : `⚠️ **${totalRisks} potential risks found** — review flagged plugins`)
162
+ lines.push(zh
163
+ ? '💡 建议: 仅从可信渠道安装插件,定期运行 `/scan-plugins` 检查'
164
+ : '💡 Tip: Only install plugins from trusted sources, run `/scan-plugins` regularly')
165
+ }
166
+
167
+ return { text: lines.join('\n') }
168
+ },
169
+ })
170
+ }
171
+
172
+ function collectSourceFiles(dir: string, maxDepth: number, depth = 0): string[] {
173
+ if (depth > maxDepth) return []
174
+ const files: string[] = []
175
+ try {
176
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
177
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') continue
178
+ const full = join(dir, entry.name)
179
+ if (entry.isDirectory()) {
180
+ files.push(...collectSourceFiles(full, maxDepth, depth + 1))
181
+ } else if (/\.(ts|js|mjs|cjs)$/.test(entry.name)) {
182
+ files.push(full)
183
+ }
184
+ }
185
+ } catch { /* skip */ }
186
+ return files
187
+ }
@@ -0,0 +1,113 @@
1
+ // src/commands/security.ts — /security command: full security status overview
2
+
3
+ import { readFileSync, statSync, existsSync, readdirSync } from 'fs'
4
+ import { join } from 'path'
5
+ import { execSync } from 'child_process'
6
+ import type { ShellWardConfig } from '../types'
7
+ import { resolveLocale } from '../types'
8
+
9
+ const LOG_DIR = join(process.env.HOME || '~', '.openclaw', 'shellward')
10
+ const LOG_FILE = join(LOG_DIR, 'audit.jsonl')
11
+
12
+ export function registerSecurityCommand(api: any, config: ShellWardConfig) {
13
+ const locale = resolveLocale(config)
14
+
15
+ api.registerCommand({
16
+ name: 'security',
17
+ description: locale === 'zh'
18
+ ? '🛡️ ShellWard 安全状态总览'
19
+ : '🛡️ ShellWard security status overview',
20
+ acceptsArgs: false,
21
+ handler: () => {
22
+ const lines: string[] = []
23
+ const zh = locale === 'zh'
24
+
25
+ lines.push(zh ? '🛡️ **ShellWard 安全状态报告**' : '🛡️ **ShellWard Security Status Report**')
26
+ lines.push('')
27
+
28
+ // 1. Layer status
29
+ lines.push(zh ? '## 防御层状态' : '## Defense Layers')
30
+ const layers = [
31
+ ['L1 Prompt Guard', config.layers.promptGuard],
32
+ ['L2 Output Scanner', config.layers.outputScanner],
33
+ ['L3 Tool Blocker', config.layers.toolBlocker],
34
+ ['L4 Input Auditor', config.layers.inputAuditor],
35
+ ['L5 Security Gate', config.layers.securityGate],
36
+ ]
37
+ for (const [name, enabled] of layers) {
38
+ lines.push(` ${enabled ? '✅' : '❌'} ${name}`)
39
+ }
40
+ lines.push(` ${zh ? '模式' : 'Mode'}: **${config.mode}**`)
41
+ lines.push(` ${zh ? '注入阈值' : 'Injection threshold'}: **${config.injectionThreshold}**`)
42
+ lines.push('')
43
+
44
+ // 2. Audit log stats
45
+ lines.push(zh ? '## 审计日志' : '## Audit Log')
46
+ try {
47
+ const stat = statSync(LOG_FILE)
48
+ const sizeMB = (stat.size / 1024 / 1024).toFixed(2)
49
+ lines.push(` ${zh ? '文件' : 'File'}: ${LOG_FILE}`)
50
+ lines.push(` ${zh ? '大小' : 'Size'}: ${sizeMB} MB`)
51
+
52
+ // Count recent events
53
+ const content = readFileSync(LOG_FILE, 'utf-8')
54
+ const allLines = content.trim().split('\n').filter(Boolean)
55
+ const total = allLines.length
56
+ let blocks = 0
57
+ let redacts = 0
58
+ let criticals = 0
59
+ for (const line of allLines) {
60
+ if (line.includes('"action":"block"')) blocks++
61
+ if (line.includes('"action":"redact"')) redacts++
62
+ if (line.includes('"level":"CRITICAL"')) criticals++
63
+ }
64
+ lines.push(` ${zh ? '总事件' : 'Total events'}: ${total}`)
65
+ lines.push(` ${zh ? '拦截' : 'Blocked'}: ${blocks} | ${zh ? '脱敏' : 'Redacted'}: ${redacts} | ${zh ? '严重' : 'Critical'}: ${criticals}`)
66
+ } catch {
67
+ lines.push(zh ? ' ⚠️ 日志文件不存在' : ' ⚠️ Log file not found')
68
+ }
69
+ lines.push('')
70
+
71
+ // 3. Quick system security checks
72
+ lines.push(zh ? '## 系统安全快检' : '## Quick System Security Check')
73
+
74
+ // Check exposed ports
75
+ try {
76
+ const listening = execSync('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null', { timeout: 5000 }).toString()
77
+ const openClawPort = listening.includes(':19000') || listening.includes(':19001')
78
+ lines.push(openClawPort
79
+ ? (zh ? ' ⚠️ OpenClaw 端口正在监听 (建议限制访问)' : ' ⚠️ OpenClaw port is listening (restrict access)')
80
+ : (zh ? ' ✅ OpenClaw 端口未暴露' : ' ✅ OpenClaw port not exposed'))
81
+ } catch {
82
+ lines.push(zh ? ' ℹ️ 无法检查端口状态' : ' ℹ️ Cannot check port status')
83
+ }
84
+
85
+ // Check .env exposure
86
+ const envFile = join(process.cwd(), '.env')
87
+ if (existsSync(envFile)) {
88
+ try {
89
+ const mode = statSync(envFile).mode & 0o777
90
+ lines.push(mode > 0o600
91
+ ? (zh ? ` ⚠️ .env 文件权限过宽 (${mode.toString(8)}),建议 chmod 600` : ` ⚠️ .env file permissions too open (${mode.toString(8)}), suggest chmod 600`)
92
+ : (zh ? ' ✅ .env 文件权限正常' : ' ✅ .env file permissions OK'))
93
+ } catch { /* skip */ }
94
+ }
95
+
96
+ // Check if running as root
97
+ if (process.getuid && process.getuid() === 0) {
98
+ lines.push(zh
99
+ ? ' ⚠️ 正在以 root 运行 (建议使用普通用户 + 容器隔离)'
100
+ : ' ⚠️ Running as root (use non-root user + container isolation)')
101
+ } else {
102
+ lines.push(zh ? ' ✅ 非 root 运行' : ' ✅ Not running as root')
103
+ }
104
+
105
+ lines.push('')
106
+ lines.push(zh
107
+ ? '💡 使用 `/audit` 查看日志, `/harden` 执行安全加固, `/scan-plugins` 扫描插件'
108
+ : '💡 Use `/audit` for logs, `/harden` for hardening, `/scan-plugins` to scan plugins')
109
+
110
+ return { text: lines.join('\n') }
111
+ },
112
+ })
113
+ }
package/src/index.ts ADDED
@@ -0,0 +1,119 @@
1
+ // src/index.ts — ShellWard plugin entry point (v0.3.1)
2
+ // 8 defense layers + 6 slash commands + 1 security skill
3
+
4
+ import { AuditLog } from './audit-log'
5
+ import { setupPromptGuard } from './layers/prompt-guard'
6
+ import { setupOutputScanner } from './layers/output-scanner'
7
+ import { setupToolBlocker } from './layers/tool-blocker'
8
+ import { setupInputAuditor } from './layers/input-auditor'
9
+ import { setupSecurityGate } from './layers/security-gate'
10
+ import { setupOutboundGuard } from './layers/outbound-guard'
11
+ import { setupDataFlowGuard } from './layers/data-flow-guard'
12
+ import { setupSessionGuard } from './layers/session-guard'
13
+ import { registerAllCommands } from './commands/index'
14
+ import { DEFAULT_CONFIG, resolveLocale } from './types'
15
+ import type { ShellWardConfig } from './types'
16
+
17
+ function mergeConfig(userConfig: Partial<ShellWardConfig> | undefined): ShellWardConfig {
18
+ if (!userConfig) return { ...DEFAULT_CONFIG }
19
+
20
+ // Validate mode
21
+ const mode = userConfig.mode === 'audit' ? 'audit' : 'enforce'
22
+
23
+ // Validate locale
24
+ const validLocales = ['auto', 'zh', 'en'] as const
25
+ const locale = validLocales.includes(userConfig.locale as any)
26
+ ? (userConfig.locale as typeof validLocales[number])
27
+ : DEFAULT_CONFIG.locale
28
+
29
+ // Validate injectionThreshold: clamp to 0-100
30
+ let threshold = userConfig.injectionThreshold ?? DEFAULT_CONFIG.injectionThreshold
31
+ threshold = Math.max(0, Math.min(100, Math.round(threshold)))
32
+
33
+ return {
34
+ mode,
35
+ locale,
36
+ injectionThreshold: threshold,
37
+ layers: {
38
+ ...DEFAULT_CONFIG.layers,
39
+ ...(userConfig.layers || {}),
40
+ },
41
+ }
42
+ }
43
+
44
+ export default {
45
+ id: 'shellward',
46
+
47
+ register(api: any) {
48
+ const config = mergeConfig(api.config)
49
+ const log = new AuditLog(config)
50
+ const enforce = config.mode === 'enforce'
51
+ const locale = resolveLocale(config)
52
+
53
+ const modeLabel = locale === 'zh'
54
+ ? `模式: ${config.mode}`
55
+ : `mode: ${config.mode}`
56
+ api.logger.info(`[ShellWard] Security plugin started (${modeLabel})`)
57
+
58
+ // === Defense Layers (L1-L8) ===
59
+
60
+ // L1: Prompt Guard (before_prompt_build — prependSystemContext for caching)
61
+ if (config.layers.promptGuard) {
62
+ setupPromptGuard(api, config, log)
63
+ }
64
+
65
+ // L2: Output Scanner (tool_result_persist — redact PII in tool results)
66
+ if (config.layers.outputScanner) {
67
+ setupOutputScanner(api, config, log, enforce)
68
+ }
69
+
70
+ // L3: Tool Blocker (before_tool_call — block dangerous commands/paths)
71
+ if (config.layers.toolBlocker) {
72
+ setupToolBlocker(api, config, log, enforce)
73
+ }
74
+
75
+ // L4: Input Auditor (before_tool_call + message_received — injection detection)
76
+ if (config.layers.inputAuditor) {
77
+ setupInputAuditor(api, config, log, enforce)
78
+ }
79
+
80
+ // L5: Security Gate (registerTool — defense in depth)
81
+ if (config.layers.securityGate) {
82
+ setupSecurityGate(api, config, log, enforce)
83
+ }
84
+
85
+ // L6: Outbound Guard (message_sending — redact PII in LLM responses + canary detection)
86
+ if (config.layers.outboundGuard) {
87
+ setupOutboundGuard(api, config, log, enforce)
88
+ }
89
+
90
+ // L7: Data Flow Guard (after_tool_call + before_tool_call — anti-exfiltration)
91
+ if (config.layers.dataFlowGuard) {
92
+ setupDataFlowGuard(api, config, log, enforce)
93
+ }
94
+
95
+ // L8: Session Guard (session_end + subagent_spawning — lifecycle security)
96
+ if (config.layers.sessionGuard) {
97
+ setupSessionGuard(api, config, log, enforce)
98
+ }
99
+
100
+ // === Slash Commands ===
101
+ if (api.registerCommand) {
102
+ registerAllCommands(api, config)
103
+ api.logger.info('[ShellWard] 6 commands registered: /security /audit /harden /scan-plugins /check-updates /cg')
104
+ }
105
+
106
+ // Count enabled layers
107
+ const allLayers = ['promptGuard', 'outputScanner', 'toolBlocker', 'inputAuditor', 'securityGate', 'outboundGuard', 'dataFlowGuard', 'sessionGuard']
108
+ const enabledCount = allLayers.filter(k => (config.layers as any)[k]).length
109
+
110
+ api.logger.info(`[ShellWard] ${enabledCount} defense layers active`)
111
+
112
+ log.write({
113
+ level: 'INFO',
114
+ layer: 'L1',
115
+ action: 'allow',
116
+ detail: `ShellWard v0.3.2 started with ${enabledCount} layers`,
117
+ })
118
+ },
119
+ }