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.
- package/LICENSE +190 -0
- package/README.md +319 -0
- package/install.ps1 +84 -0
- package/install.sh +201 -0
- package/openclaw.plugin.json +47 -0
- package/package.json +39 -0
- package/skills/security-guide/SKILL.md +68 -0
- package/src/audit-log.ts +57 -0
- package/src/commands/audit.ts +79 -0
- package/src/commands/check-updates.ts +151 -0
- package/src/commands/harden.ts +192 -0
- package/src/commands/index.ts +57 -0
- package/src/commands/scan-plugins.ts +187 -0
- package/src/commands/security.ts +113 -0
- package/src/index.ts +119 -0
- package/src/layers/data-flow-guard.ts +159 -0
- package/src/layers/input-auditor.ts +171 -0
- package/src/layers/outbound-guard.ts +67 -0
- package/src/layers/output-scanner.ts +94 -0
- package/src/layers/prompt-guard.ts +71 -0
- package/src/layers/security-gate.ts +131 -0
- package/src/layers/session-guard.ts +46 -0
- package/src/layers/tool-blocker.ts +182 -0
- package/src/rules/dangerous-commands.ts +105 -0
- package/src/rules/injection-en.ts +102 -0
- package/src/rules/injection-zh.ts +99 -0
- package/src/rules/protected-paths.ts +78 -0
- package/src/rules/sensitive-patterns.ts +204 -0
- package/src/types.ts +93 -0
|
@@ -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
|
+
}
|