shellward 0.5.10 → 0.5.11

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.
Files changed (85) hide show
  1. package/README.md +99 -14
  2. package/dist/audit-log.d.ts +8 -0
  3. package/dist/audit-log.js +72 -0
  4. package/dist/auto-check.d.ts +26 -0
  5. package/dist/auto-check.js +167 -0
  6. package/dist/commands/audit.d.ts +2 -0
  7. package/dist/commands/audit.js +75 -0
  8. package/dist/commands/check-updates.d.ts +2 -0
  9. package/dist/commands/check-updates.js +166 -0
  10. package/dist/commands/harden.d.ts +2 -0
  11. package/dist/commands/harden.js +218 -0
  12. package/dist/commands/index.d.ts +2 -0
  13. package/dist/commands/index.js +56 -0
  14. package/dist/commands/scan-plugins.d.ts +2 -0
  15. package/dist/commands/scan-plugins.js +186 -0
  16. package/dist/commands/security.d.ts +2 -0
  17. package/dist/commands/security.js +109 -0
  18. package/dist/commands/upgrade-openclaw.d.ts +2 -0
  19. package/dist/commands/upgrade-openclaw.js +54 -0
  20. package/dist/core/engine.d.ts +66 -0
  21. package/dist/core/engine.js +572 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +137 -0
  24. package/dist/layers/data-flow-guard.d.ts +2 -0
  25. package/dist/layers/data-flow-guard.js +23 -0
  26. package/dist/layers/input-auditor.d.ts +2 -0
  27. package/dist/layers/input-auditor.js +33 -0
  28. package/dist/layers/outbound-guard.d.ts +2 -0
  29. package/dist/layers/outbound-guard.js +22 -0
  30. package/dist/layers/output-scanner.d.ts +2 -0
  31. package/dist/layers/output-scanner.js +16 -0
  32. package/dist/layers/prompt-guard.d.ts +2 -0
  33. package/dist/layers/prompt-guard.js +14 -0
  34. package/dist/layers/security-gate.d.ts +2 -0
  35. package/dist/layers/security-gate.js +49 -0
  36. package/dist/layers/session-guard.d.ts +2 -0
  37. package/dist/layers/session-guard.js +34 -0
  38. package/dist/layers/tool-blocker.d.ts +2 -0
  39. package/dist/layers/tool-blocker.js +28 -0
  40. package/dist/mcp-server.d.ts +2 -0
  41. package/dist/mcp-server.js +337 -0
  42. package/dist/rules/dangerous-commands.d.ts +8 -0
  43. package/dist/rules/dangerous-commands.js +113 -0
  44. package/dist/rules/injection-en.d.ts +2 -0
  45. package/dist/rules/injection-en.js +115 -0
  46. package/dist/rules/injection-zh.d.ts +2 -0
  47. package/dist/rules/injection-zh.js +132 -0
  48. package/dist/rules/protected-paths.d.ts +2 -0
  49. package/dist/rules/protected-paths.js +75 -0
  50. package/dist/rules/sensitive-patterns.d.ts +21 -0
  51. package/dist/rules/sensitive-patterns.js +192 -0
  52. package/dist/types.d.ts +64 -0
  53. package/dist/types.js +30 -0
  54. package/dist/update-check.d.ts +40 -0
  55. package/dist/update-check.js +147 -0
  56. package/dist/utils.d.ts +4 -0
  57. package/dist/utils.js +8 -0
  58. package/openclaw.plugin.json +1 -1
  59. package/package.json +18 -6
  60. package/src/audit-log.ts +8 -4
  61. package/src/auto-check.ts +2 -2
  62. package/src/commands/audit.ts +3 -3
  63. package/src/commands/check-updates.ts +4 -4
  64. package/src/commands/harden.ts +3 -3
  65. package/src/commands/index.ts +8 -8
  66. package/src/commands/scan-plugins.ts +3 -3
  67. package/src/commands/security.ts +3 -3
  68. package/src/commands/upgrade-openclaw.ts +2 -2
  69. package/src/core/engine.ts +8 -8
  70. package/src/index.ts +15 -15
  71. package/src/layers/data-flow-guard.ts +1 -1
  72. package/src/layers/input-auditor.ts +1 -1
  73. package/src/layers/outbound-guard.ts +1 -1
  74. package/src/layers/output-scanner.ts +1 -1
  75. package/src/layers/prompt-guard.ts +1 -1
  76. package/src/layers/security-gate.ts +1 -1
  77. package/src/layers/session-guard.ts +1 -1
  78. package/src/layers/tool-blocker.ts +1 -1
  79. package/src/mcp-server.ts +386 -0
  80. package/src/rules/dangerous-commands.ts +1 -1
  81. package/src/rules/injection-en.ts +1 -1
  82. package/src/rules/injection-zh.ts +1 -1
  83. package/src/rules/protected-paths.ts +1 -1
  84. package/src/rules/sensitive-patterns.ts +1 -1
  85. package/src/update-check.ts +1 -1
@@ -0,0 +1,166 @@
1
+ // src/commands/check-updates.ts — /check-updates: check versions + remote vulnerability DB
2
+ import { execSync } from 'child_process';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { resolveLocale } from '../types.js';
6
+ import { checkForUpdate, fetchVulnDB, compareVersions } from '../update-check.js';
7
+ // Local fallback vulnerability database (used when remote fetch fails)
8
+ // Contains only CVE-assigned vulnerabilities as minimum baseline
9
+ const LOCAL_VULNS = [
10
+ {
11
+ affectedBelow: '1.0.111',
12
+ severity: 'HIGH',
13
+ id: 'CVE-2025-59536',
14
+ description_zh: '远程代码执行:恶意仓库通过 Hooks 和 MCP Server 在信任提示前执行任意命令 (CVSS 8.7)',
15
+ description_en: 'RCE via Hooks and MCP Server bypass — arbitrary shell execution before trust dialog (CVSS 8.7)',
16
+ },
17
+ {
18
+ affectedBelow: '2.0.65',
19
+ severity: 'MEDIUM',
20
+ id: 'CVE-2026-21852',
21
+ description_zh: 'API 密钥泄露:恶意仓库通过 settings.json 设置 ANTHROPIC_BASE_URL 窃取用户 API Key (CVSS 5.3)',
22
+ description_en: 'API key exfiltration via ANTHROPIC_BASE_URL in settings.json before trust prompt (CVSS 5.3)',
23
+ },
24
+ {
25
+ affectedBelow: '2026.2.7',
26
+ severity: 'HIGH',
27
+ id: 'GHSA-ff64-7w26-62rf',
28
+ description_zh: '沙箱逃逸:通过 settings.json 持久化配置注入',
29
+ description_en: 'Sandbox escape via persistent configuration injection in settings.json',
30
+ },
31
+ ];
32
+ export function registerCheckUpdatesCommand(api, config) {
33
+ const locale = resolveLocale(config);
34
+ api.registerCommand({
35
+ name: 'check-updates',
36
+ description: locale === 'zh'
37
+ ? '🔄 检查版本更新和已知漏洞(支持远程漏洞库)'
38
+ : '🔄 Check for updates and known vulnerabilities (remote vuln DB)',
39
+ acceptsArgs: false,
40
+ handler: async () => {
41
+ const zh = locale === 'zh';
42
+ const lines = [];
43
+ lines.push(zh ? '🔄 **版本与漏洞检查**' : '🔄 **Version & Vulnerability Check**');
44
+ lines.push('');
45
+ // 1. Get OpenClaw version
46
+ let openclawVersion = 'unknown';
47
+ try {
48
+ const out = execSync('openclaw --version 2>&1', { timeout: 5000 }).toString().trim();
49
+ const match = out.match(/(\d{4}\.\d+\.\d+)/);
50
+ if (match)
51
+ openclawVersion = match[1];
52
+ }
53
+ catch { /* skip */ }
54
+ lines.push(zh
55
+ ? `### OpenClaw 版本: ${openclawVersion}`
56
+ : `### OpenClaw Version: ${openclawVersion}`);
57
+ lines.push('');
58
+ // 2. Check ShellWard version + available update
59
+ let shellwardVersion = 'unknown';
60
+ try {
61
+ const pkgPath = join(__dirname, '../../package.json');
62
+ if (existsSync(pkgPath)) {
63
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
64
+ shellwardVersion = pkg.version || 'unknown';
65
+ }
66
+ }
67
+ catch { /* skip */ }
68
+ lines.push(zh
69
+ ? `### ShellWard 版本: ${shellwardVersion}`
70
+ : `### ShellWard Version: ${shellwardVersion}`);
71
+ // Check for ShellWard update from npm
72
+ try {
73
+ const updateInfo = await checkForUpdate(shellwardVersion);
74
+ if (updateInfo?.updateAvailable) {
75
+ lines.push(zh
76
+ ? ` 🆕 **新版本 v${updateInfo.latest} 可用!** 运行 \`openclaw plugins update shellward\` 更新`
77
+ : ` 🆕 **v${updateInfo.latest} available!** Run \`openclaw plugins update shellward\` to update`);
78
+ }
79
+ else if (updateInfo) {
80
+ lines.push(zh ? ' ✅ 已是最新版本' : ' ✅ Up to date');
81
+ }
82
+ }
83
+ catch { /* skip */ }
84
+ lines.push('');
85
+ // 3. Check known vulnerabilities (remote DB with local fallback)
86
+ lines.push(zh ? '### 已知漏洞检查' : '### Known Vulnerability Check');
87
+ let vulnDB = LOCAL_VULNS;
88
+ let alerts = [];
89
+ let dbSource = 'local';
90
+ try {
91
+ const remote = await fetchVulnDB();
92
+ if (remote.vulns.length > 0) {
93
+ vulnDB = remote.vulns;
94
+ dbSource = 'remote';
95
+ }
96
+ alerts = remote.alerts || [];
97
+ }
98
+ catch { /* use local */ }
99
+ lines.push(zh
100
+ ? ` 数据源: ${dbSource === 'remote' ? `远程漏洞库 (GitHub) — ${vulnDB.length} 条记录` : '本地内置数据库'}`
101
+ : ` Source: ${dbSource === 'remote' ? `Remote vuln DB (GitHub) — ${vulnDB.length} entries` : 'Local built-in database'}`);
102
+ if (openclawVersion === 'unknown') {
103
+ lines.push(zh
104
+ ? ' ⚠️ 无法确定 OpenClaw 版本,请手动检查'
105
+ : ' ⚠️ Cannot determine OpenClaw version, please check manually');
106
+ }
107
+ else {
108
+ const affected = vulnDB.filter(v => compareVersions(openclawVersion, v.affectedBelow) < 0);
109
+ if (affected.length === 0) {
110
+ lines.push(zh
111
+ ? ' ✅ 当前版本未发现已知漏洞'
112
+ : ' ✅ No known vulnerabilities for current version');
113
+ }
114
+ else {
115
+ for (const vuln of affected) {
116
+ const icon = vuln.severity === 'CRITICAL' ? '🔴' : '🟡';
117
+ const desc = zh ? vuln.description_zh : vuln.description_en;
118
+ lines.push(` ${icon} **${vuln.id}** [${vuln.severity}]: ${desc}`);
119
+ lines.push(zh
120
+ ? ` 影响版本: < ${vuln.affectedBelow} — 请升级`
121
+ : ` Affected: < ${vuln.affectedBelow} — please upgrade`);
122
+ }
123
+ }
124
+ }
125
+ // Supply chain alerts
126
+ if (alerts.length > 0) {
127
+ lines.push('');
128
+ lines.push(zh ? '### 供应链安全警告' : '### Supply Chain Alerts');
129
+ for (const alert of alerts) {
130
+ const icon = alert.severity === 'CRITICAL' ? '🔴' : '🟡';
131
+ const desc = zh ? alert.description_zh : alert.description_en;
132
+ lines.push(` ${icon} **${alert.id}** [${alert.date}]: ${desc}`);
133
+ }
134
+ }
135
+ lines.push('');
136
+ // 4. Check Node.js version
137
+ lines.push(zh ? '### 运行环境' : '### Runtime Environment');
138
+ try {
139
+ const nodeVer = process.version;
140
+ const major = parseInt(nodeVer.slice(1));
141
+ if (major < 22) {
142
+ lines.push(zh
143
+ ? ` ⚠️ Node.js ${nodeVer} — OpenClaw 要求 >= 22.12,请升级`
144
+ : ` ⚠️ Node.js ${nodeVer} — OpenClaw requires >= 22.12, please upgrade`);
145
+ }
146
+ else {
147
+ lines.push(zh
148
+ ? ` ✅ Node.js ${nodeVer}`
149
+ : ` ✅ Node.js ${nodeVer}`);
150
+ }
151
+ }
152
+ catch { /* skip */ }
153
+ lines.push(` ${zh ? '平台' : 'Platform'}: ${process.platform} ${process.arch}`);
154
+ lines.push('');
155
+ // 5. Recommendations
156
+ lines.push('---');
157
+ lines.push(zh
158
+ ? '💡 **建议**: 定期运行 `/check-updates` 检查,及时升级到最新版本'
159
+ : '💡 **Tip**: Run `/check-updates` regularly, upgrade to latest versions promptly');
160
+ lines.push(zh
161
+ ? '📖 关注 OpenClaw 安全公告: https://github.com/nicepkg/openclaw/security'
162
+ : '📖 Follow OpenClaw security advisories: https://github.com/nicepkg/openclaw/security');
163
+ return { text: lines.join('\n') };
164
+ },
165
+ });
166
+ }
@@ -0,0 +1,2 @@
1
+ import type { ShellWardConfig } from '../types.js';
2
+ export declare function registerHardenCommand(api: any, config: ShellWardConfig): void;
@@ -0,0 +1,218 @@
1
+ // src/commands/harden.ts — /harden command: one-click security hardening
2
+ import { existsSync, statSync, chmodSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { resolveLocale } from '../types.js';
6
+ import { getHomeDir } from '../utils.js';
7
+ const HOME = getHomeDir();
8
+ export function registerHardenCommand(api, config) {
9
+ const locale = resolveLocale(config);
10
+ api.registerCommand({
11
+ name: 'harden',
12
+ description: locale === 'zh'
13
+ ? '🔒 一键安全加固 (权限修复、凭证扫描、端口检查)'
14
+ : '🔒 One-click security hardening (permissions, credentials, ports)',
15
+ acceptsArgs: true,
16
+ handler: (ctx) => {
17
+ const zh = locale === 'zh';
18
+ const args = (ctx.args || '').trim().toLowerCase();
19
+ const dryRun = args !== 'fix';
20
+ const lines = [];
21
+ const issues = [];
22
+ const fixed = [];
23
+ lines.push(zh ? '🔒 **安全加固扫描**' : '🔒 **Security Hardening Scan**');
24
+ if (dryRun) {
25
+ lines.push(zh
26
+ ? '_(扫描模式 — 使用 `/harden fix` 自动修复)_'
27
+ : '_(scan mode — use `/harden fix` to auto-fix)_');
28
+ }
29
+ lines.push('');
30
+ // === 1. File Permission Checks ===
31
+ lines.push(zh ? '### 文件权限检查' : '### File Permission Checks');
32
+ const sensitiveFiles = [
33
+ ['.env', 0o600],
34
+ ['.env.local', 0o600],
35
+ ['.env.production', 0o600],
36
+ ['.ssh/id_rsa', 0o600],
37
+ ['.ssh/id_ed25519', 0o600],
38
+ ['.ssh/config', 0o600],
39
+ ['.aws/credentials', 0o600],
40
+ ['.npmrc', 0o600],
41
+ ['.git-credentials', 0o600],
42
+ ['.openclaw/openclaw.json', 0o600],
43
+ ];
44
+ for (const [rel, target] of sensitiveFiles) {
45
+ const full = join(HOME, rel);
46
+ if (!existsSync(full))
47
+ continue;
48
+ try {
49
+ const stat = statSync(full);
50
+ const current = stat.mode & 0o777;
51
+ if (current > target) {
52
+ const msg = zh
53
+ ? `⚠️ ${rel}: 权限 ${current.toString(8)} → 建议 ${target.toString(8)}`
54
+ : `⚠️ ${rel}: permissions ${current.toString(8)} → should be ${target.toString(8)}`;
55
+ issues.push(msg);
56
+ lines.push(` ${msg}`);
57
+ if (!dryRun) {
58
+ try {
59
+ chmodSync(full, target);
60
+ fixed.push(rel);
61
+ lines.push(zh ? ` ✅ 已修复` : ` ✅ Fixed`);
62
+ }
63
+ catch {
64
+ lines.push(zh ? ` ❌ 修复失败(权限不足)` : ` ❌ Fix failed (permission denied)`);
65
+ }
66
+ }
67
+ }
68
+ else {
69
+ lines.push(zh ? ` ✅ ${rel}: ${current.toString(8)}` : ` ✅ ${rel}: ${current.toString(8)}`);
70
+ }
71
+ }
72
+ catch { /* skip */ }
73
+ }
74
+ lines.push('');
75
+ // === 2. Plaintext Credential Scan ===
76
+ lines.push(zh ? '### 明文凭证扫描' : '### Plaintext Credential Scan');
77
+ const credPatterns = [
78
+ /(?:api[_-]?key|api[_-]?token|access[_-]?token)\s*[=:]\s*['"]?[A-Za-z0-9_\-]{20,}/i,
79
+ /sk-[a-zA-Z0-9]{20,}/,
80
+ /AKIA[0-9A-Z]{16}/,
81
+ /ghp_[A-Za-z0-9_]{36,}/,
82
+ /password\s*[=:]\s*['"]?\S{6,}/i,
83
+ ];
84
+ const envFiles = ['.env', '.env.local', '.env.production', '.bashrc', '.zshrc', '.bash_profile'];
85
+ let credFound = 0;
86
+ for (const rel of envFiles) {
87
+ const full = join(HOME, rel);
88
+ if (!existsSync(full))
89
+ continue;
90
+ try {
91
+ const content = readFileSync(full, 'utf-8');
92
+ for (const pat of credPatterns) {
93
+ if (pat.test(content)) {
94
+ credFound++;
95
+ const msg = zh
96
+ ? `⚠️ ${rel}: 发现明文凭证(建议使用密钥管理工具)`
97
+ : `⚠️ ${rel}: plaintext credentials found (use a secret manager)`;
98
+ issues.push(msg);
99
+ lines.push(` ${msg}`);
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ catch { /* skip */ }
105
+ }
106
+ if (credFound === 0) {
107
+ lines.push(zh ? ' ✅ 未发现明文凭证' : ' ✅ No plaintext credentials found');
108
+ }
109
+ lines.push('');
110
+ // === 3. Network Exposure ===
111
+ lines.push(zh ? '### 网络暴露检查' : '### Network Exposure Check');
112
+ try {
113
+ const listening = execSync('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null', { timeout: 5000 }).toString();
114
+ const dangerPorts = [
115
+ ['19000', 'OpenClaw Gateway'],
116
+ ['19001', 'OpenClaw Dev'],
117
+ ['3000', 'Dev Server'],
118
+ ['8080', 'HTTP Alt'],
119
+ ['5432', 'PostgreSQL'],
120
+ ['3306', 'MySQL'],
121
+ ['6379', 'Redis'],
122
+ ['27017', 'MongoDB'],
123
+ ];
124
+ let portIssues = 0;
125
+ for (const [port, name] of dangerPorts) {
126
+ // Check if listening on 0.0.0.0 (all interfaces)
127
+ const allInterfaces = listening.includes(`0.0.0.0:${port}`) || listening.includes(`:::${port}`);
128
+ if (allInterfaces) {
129
+ portIssues++;
130
+ const msg = zh
131
+ ? `⚠️ ${name} (${port}) 监听在所有接口 — 建议绑定 127.0.0.1`
132
+ : `⚠️ ${name} (${port}) listening on all interfaces — bind to 127.0.0.1`;
133
+ issues.push(msg);
134
+ lines.push(` ${msg}`);
135
+ }
136
+ }
137
+ if (portIssues === 0) {
138
+ lines.push(zh ? ' ✅ 未发现危险端口暴露' : ' ✅ No dangerous port exposure detected');
139
+ }
140
+ }
141
+ catch {
142
+ lines.push(zh ? ' ℹ️ 无法检查网络状态' : ' ℹ️ Cannot check network status');
143
+ }
144
+ lines.push('');
145
+ // === 4. Running as root ===
146
+ lines.push(zh ? '### 运行权限' : '### Runtime Privileges');
147
+ if (process.getuid && process.getuid() === 0) {
148
+ const msg = zh
149
+ ? '⚠️ 以 root 身份运行 — 强烈建议使用普通用户 + 容器隔离'
150
+ : '⚠️ Running as root — strongly recommend non-root user + container isolation';
151
+ issues.push(msg);
152
+ lines.push(` ${msg}`);
153
+ }
154
+ else {
155
+ lines.push(zh ? ' ✅ 非 root 运行' : ' ✅ Not running as root');
156
+ }
157
+ // Check if Docker available
158
+ try {
159
+ execSync('which docker 2>/dev/null', { timeout: 3000 });
160
+ lines.push(zh ? ' ✅ Docker 可用(可用于容器隔离)' : ' ✅ Docker available (for container isolation)');
161
+ }
162
+ catch {
163
+ lines.push(zh ? ' ℹ️ Docker 未安装(建议安装以支持容器隔离)' : ' ℹ️ Docker not installed (recommended for isolation)');
164
+ }
165
+ lines.push('');
166
+ // === 5. One-click scripts ===
167
+ lines.push(zh ? '### 一键安全脚本' : '### One-click Security Scripts');
168
+ // Dockerfile
169
+ lines.push(zh ? '**容器隔离** — 复制以下 Dockerfile:' : '**Container isolation** — copy this Dockerfile:');
170
+ lines.push('```dockerfile');
171
+ lines.push('FROM node:22-slim');
172
+ lines.push('RUN useradd -m -s /bin/bash openclaw');
173
+ lines.push('USER openclaw');
174
+ lines.push('WORKDIR /home/openclaw');
175
+ lines.push('RUN npm install -g openclaw');
176
+ lines.push('COPY .env .env');
177
+ lines.push('EXPOSE 19000');
178
+ lines.push('CMD ["openclaw", "agent", "--local"]');
179
+ lines.push('```');
180
+ lines.push(zh
181
+ ? '运行: `docker build -t openclaw-safe . && docker run --rm -it openclaw-safe`'
182
+ : 'Run: `docker build -t openclaw-safe . && docker run --rm -it openclaw-safe`');
183
+ lines.push('');
184
+ // Firewall
185
+ lines.push(zh ? '**防火墙限制** — 仅允许必要出站:' : '**Firewall** — allow only necessary outbound:');
186
+ lines.push('```bash');
187
+ lines.push(zh ? '# 只允许 HTTPS 出站(API 调用),禁止其他出站' : '# Allow only HTTPS outbound (API calls), block everything else');
188
+ lines.push('sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT');
189
+ lines.push('sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT');
190
+ lines.push('sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT # DNS');
191
+ lines.push('sudo iptables -A OUTPUT -o lo -j ACCEPT # localhost');
192
+ lines.push('sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT');
193
+ lines.push('sudo iptables -A OUTPUT -j LOG --log-prefix "BLOCKED: "');
194
+ lines.push('sudo iptables -A OUTPUT -j DROP');
195
+ lines.push('```');
196
+ lines.push(zh
197
+ ? '⚠️ 执行前请确认不会影响其他服务'
198
+ : '⚠️ Review before applying — may affect other services');
199
+ lines.push('');
200
+ // === Summary ===
201
+ lines.push('---');
202
+ if (issues.length === 0) {
203
+ lines.push(zh ? '✅ **安全检查通过!未发现问题。**' : '✅ **All checks passed! No issues found.**');
204
+ }
205
+ else {
206
+ lines.push(zh
207
+ ? `⚠️ **发现 ${issues.length} 个安全问题**${fixed.length > 0 ? `,已自动修复 ${fixed.length} 个` : ''}`
208
+ : `⚠️ **Found ${issues.length} security issues**${fixed.length > 0 ? `, auto-fixed ${fixed.length}` : ''}`);
209
+ if (dryRun && issues.some(i => i.includes('权限') || i.includes('permissions'))) {
210
+ lines.push(zh
211
+ ? '💡 使用 `/harden fix` 自动修复文件权限问题'
212
+ : '💡 Use `/harden fix` to auto-fix file permission issues');
213
+ }
214
+ }
215
+ return { text: lines.join('\n') };
216
+ },
217
+ });
218
+ }
@@ -0,0 +1,2 @@
1
+ import type { ShellWardConfig } from '../types.js';
2
+ export declare function registerAllCommands(api: any, config: ShellWardConfig): void;
@@ -0,0 +1,56 @@
1
+ // src/commands/index.ts — Register all ShellWard commands
2
+ import { resolveLocale } from '../types.js';
3
+ import { registerSecurityCommand } from './security.js';
4
+ import { registerAuditCommand } from './audit.js';
5
+ import { registerHardenCommand } from './harden.js';
6
+ import { registerScanPluginsCommand } from './scan-plugins.js';
7
+ import { registerCheckUpdatesCommand } from './check-updates.js';
8
+ import { registerUpgradeOpenClawCommand } from './upgrade-openclaw.js';
9
+ export function registerAllCommands(api, config) {
10
+ const locale = resolveLocale(config);
11
+ // Register individual commands
12
+ registerSecurityCommand(api, config);
13
+ registerAuditCommand(api, config);
14
+ registerHardenCommand(api, config);
15
+ registerScanPluginsCommand(api, config);
16
+ registerCheckUpdatesCommand(api, config);
17
+ registerUpgradeOpenClawCommand(api, config);
18
+ // Register /cg shortcut with help
19
+ api.registerCommand({
20
+ name: 'cg',
21
+ description: locale === 'zh'
22
+ ? '🛡️ ShellWard 安全命令帮助'
23
+ : '🛡️ ShellWard security command help',
24
+ acceptsArgs: false,
25
+ handler: () => ({
26
+ text: locale === 'zh' ? `🛡️ **ShellWard 快捷命令**
27
+
28
+ | 命令 | 说明 |
29
+ |------|------|
30
+ | \`/security\` | 安全状态总览(防御层、审计统计、系统检查) |
31
+ | \`/audit [数量] [过滤]\` | 查看审计日志 (过滤: block/audit/critical/high) |
32
+ | \`/harden\` | 安全扫描 · \`/harden fix\` 自动修复权限 |
33
+ | \`/scan-plugins\` | 扫描已安装插件的安全风险 |
34
+ | \`/check-updates\` | 检查 OpenClaw 版本和已知漏洞 |
35
+ | \`/upgrade-openclaw\` | 一键升级 OpenClaw · \`/upgrade-openclaw yes\` 直接执行 |
36
+
37
+ **当前防御层 (8层):**
38
+ L1 提示注入 · L2 输出审计 · L3 工具拦截 · L4 注入检测
39
+ L5 安全门 · L6 回复审计 · L7 数据流监控 · L8 会话安全`
40
+ : `🛡️ **ShellWard Quick Commands**
41
+
42
+ | Command | Description |
43
+ |---------|-------------|
44
+ | \`/security\` | Security status overview (layers, audit stats, system checks) |
45
+ | \`/audit [count] [filter]\` | View audit log (filter: block/audit/critical/high) |
46
+ | \`/harden\` | Security scan · \`/harden fix\` to auto-fix permissions |
47
+ | \`/scan-plugins\` | Scan installed plugins for security risks |
48
+ | \`/check-updates\` | Check OpenClaw version and known vulnerabilities |
49
+ | \`/upgrade-openclaw\` | Upgrade OpenClaw · \`/upgrade-openclaw yes\` to execute |
50
+
51
+ **Active Defense Layers (8):**
52
+ L1 Prompt Guard · L2 Output Scanner · L3 Tool Blocker · L4 Input Auditor
53
+ L5 Security Gate · L6 Outbound Guard · L7 Data Flow Guard · L8 Session Guard`,
54
+ }),
55
+ });
56
+ }
@@ -0,0 +1,2 @@
1
+ import type { ShellWardConfig } from '../types.js';
2
+ export declare function registerScanPluginsCommand(api: any, config: ShellWardConfig): void;
@@ -0,0 +1,186 @@
1
+ // src/commands/scan-plugins.ts — /scan-plugins: scan installed plugins for security risks
2
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { resolveLocale } from '../types.js';
5
+ import { getHomeDir } from '../utils.js';
6
+ const HOME = getHomeDir();
7
+ const OPENCLAW_DIR = join(HOME, '.openclaw');
8
+ // Known suspicious patterns in plugin code
9
+ const SUSPICIOUS_PATTERNS = [
10
+ { pattern: /eval\s*\(/, name: 'eval()', risk: 'code injection' },
11
+ { pattern: /child_process|execSync|spawnSync|exec\(/, name: 'shell exec', risk: 'command execution' },
12
+ { pattern: /\/dev\/tcp|nc\s+-e|ncat/, name: 'reverse shell', risk: 'remote access' },
13
+ { pattern: /fetch\s*\([^)]*(?:webhook|exfil|callback)/, name: 'data exfiltration', risk: 'data leak' },
14
+ { pattern: /process\.env\.[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)/i, name: 'env secret access', risk: 'credential access' },
15
+ { pattern: /writeFileSync.*(?:\.ssh|\.env|\.aws|\.npmrc)/, name: 'sensitive file write', risk: 'credential tampering' },
16
+ { pattern: /crypto\.createHash|Buffer\.from.*base64/, name: 'crypto/encoding', risk: 'possible obfuscation' },
17
+ { pattern: /https?:\/\/(?!(?:github\.com|npmjs\.com|registry\.npmjs\.org))[^\s'"]+/g, name: 'external URL', risk: 'data exfiltration' },
18
+ ];
19
+ export function registerScanPluginsCommand(api, config) {
20
+ const locale = resolveLocale(config);
21
+ api.registerCommand({
22
+ name: 'scan-plugins',
23
+ description: locale === 'zh'
24
+ ? '🔍 扫描已安装插件的安全风险'
25
+ : '🔍 Scan installed plugins for security risks',
26
+ acceptsArgs: false,
27
+ handler: () => {
28
+ const zh = locale === 'zh';
29
+ const lines = [];
30
+ lines.push(zh ? '🔍 **插件安全扫描报告**' : '🔍 **Plugin Security Scan Report**');
31
+ lines.push('');
32
+ // Find installed plugins
33
+ const pluginDirs = [];
34
+ // Check global extensions
35
+ const extensionsDir = join(OPENCLAW_DIR, 'extensions');
36
+ if (existsSync(extensionsDir)) {
37
+ try {
38
+ for (const name of readdirSync(extensionsDir)) {
39
+ const p = join(extensionsDir, name);
40
+ if (statSync(p).isDirectory()) {
41
+ pluginDirs.push({ name, path: p });
42
+ }
43
+ }
44
+ }
45
+ catch { /* skip */ }
46
+ }
47
+ // Check linked plugins
48
+ const pluginsDir = join(OPENCLAW_DIR, 'plugins');
49
+ if (existsSync(pluginsDir)) {
50
+ try {
51
+ for (const name of readdirSync(pluginsDir)) {
52
+ const p = join(pluginsDir, name);
53
+ pluginDirs.push({ name, path: p });
54
+ }
55
+ }
56
+ catch { /* skip */ }
57
+ }
58
+ if (pluginDirs.length === 0) {
59
+ lines.push(zh ? 'ℹ️ 未发现已安装的第三方插件。' : 'ℹ️ No third-party plugins found.');
60
+ return { text: lines.join('\n') };
61
+ }
62
+ lines.push(zh
63
+ ? `${zh ? '发现' : 'Found'} ${pluginDirs.length} ${zh ? '个插件' : 'plugins'}`
64
+ : `Found ${pluginDirs.length} plugins`);
65
+ lines.push('');
66
+ let totalRisks = 0;
67
+ const riskyPluginNames = new Set();
68
+ for (const plugin of pluginDirs) {
69
+ const risks = [];
70
+ // 1. Check for package.json
71
+ const pkgPath = join(plugin.path, 'package.json');
72
+ let pkg = null;
73
+ if (existsSync(pkgPath)) {
74
+ try {
75
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
76
+ }
77
+ catch { /* skip */ }
78
+ }
79
+ // 2. Check dependencies count (more deps = more supply chain risk)
80
+ if (pkg) {
81
+ const depCount = Object.keys(pkg.dependencies || {}).length;
82
+ if (depCount > 20) {
83
+ risks.push(zh
84
+ ? `⚠️ 依赖过多 (${depCount} 个) — 供应链攻击风险`
85
+ : `⚠️ Too many deps (${depCount}) — supply chain risk`);
86
+ }
87
+ // Check for suspicious scripts
88
+ const scripts = pkg.scripts || {};
89
+ for (const [key, val] of Object.entries(scripts)) {
90
+ if (/curl|wget|eval|nc\s/.test(String(val))) {
91
+ risks.push(zh
92
+ ? `🔴 可疑脚本: scripts.${key}`
93
+ : `🔴 Suspicious script: scripts.${key}`);
94
+ }
95
+ }
96
+ }
97
+ // 3. Scan source files for suspicious patterns
98
+ const srcFiles = collectSourceFiles(plugin.path, 3); // max depth 3
99
+ for (const file of srcFiles) {
100
+ try {
101
+ const content = readFileSync(file, 'utf-8');
102
+ for (const rule of SUSPICIOUS_PATTERNS) {
103
+ // Use fresh regex to avoid lastIndex state issues with global patterns
104
+ const regex = new RegExp(rule.pattern.source, rule.pattern.flags);
105
+ if (regex.test(content)) {
106
+ const relPath = file.replace(plugin.path + '/', '');
107
+ risks.push(zh
108
+ ? `⚠️ ${relPath}: ${rule.name} (${rule.risk})`
109
+ : `⚠️ ${relPath}: ${rule.name} (${rule.risk})`);
110
+ }
111
+ }
112
+ }
113
+ catch { /* skip */ }
114
+ }
115
+ // 4. Check if plugin has signature/checksum
116
+ const hasSignature = existsSync(join(plugin.path, 'SIGNATURE')) || existsSync(join(plugin.path, '.signature'));
117
+ if (!hasSignature) {
118
+ risks.push(zh ? 'ℹ️ 无签名验证文件' : 'ℹ️ No signature file');
119
+ }
120
+ // Output plugin report
121
+ const icon = risks.filter(r => r.startsWith('🔴')).length > 0 ? '🔴'
122
+ : risks.filter(r => r.startsWith('⚠️')).length > 0 ? '⚠️' : '✅';
123
+ lines.push(`### ${icon} ${plugin.name}`);
124
+ if (pkg) {
125
+ lines.push(` v${pkg.version || '?'} | ${pkg.author || 'unknown author'} | ${Object.keys(pkg.dependencies || {}).length} deps`);
126
+ }
127
+ if (risks.length === 0) {
128
+ lines.push(zh ? ' ✅ 未发现安全风险' : ' ✅ No security risks found');
129
+ }
130
+ else {
131
+ for (const risk of risks) {
132
+ lines.push(` ${risk}`);
133
+ totalRisks++;
134
+ }
135
+ if (risks.some(r => r.startsWith('🔴') || (r.startsWith('⚠️') && !r.includes('签名') && !r.includes('signature') && !r.includes('依赖') && !r.includes('deps')))) {
136
+ riskyPluginNames.add(plugin.name);
137
+ }
138
+ }
139
+ lines.push('');
140
+ }
141
+ // Summary + removal commands
142
+ lines.push('---');
143
+ if (totalRisks === 0) {
144
+ lines.push(zh ? '✅ **所有插件扫描通过**' : '✅ **All plugins passed scan**');
145
+ }
146
+ else {
147
+ lines.push(zh
148
+ ? `⚠️ **发现 ${totalRisks} 个潜在风险** — 请审查标记的插件`
149
+ : `⚠️ **${totalRisks} potential risks found** — review flagged plugins`);
150
+ if (riskyPluginNames.size > 0) {
151
+ lines.push('');
152
+ lines.push(zh ? '**一键移除高风险插件** — 复制执行:' : '**Remove risky plugins** — copy & run:');
153
+ lines.push('```bash');
154
+ for (const name of riskyPluginNames) {
155
+ lines.push(`openclaw plugins uninstall ${name}`);
156
+ }
157
+ lines.push('```');
158
+ }
159
+ lines.push(zh
160
+ ? '💡 建议: 仅从可信渠道安装插件,定期运行 `/scan-plugins` 检查'
161
+ : '💡 Tip: Only install plugins from trusted sources, run `/scan-plugins` regularly');
162
+ }
163
+ return { text: lines.join('\n') };
164
+ },
165
+ });
166
+ }
167
+ function collectSourceFiles(dir, maxDepth, depth = 0) {
168
+ if (depth > maxDepth)
169
+ return [];
170
+ const files = [];
171
+ try {
172
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
173
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist')
174
+ continue;
175
+ const full = join(dir, entry.name);
176
+ if (entry.isDirectory()) {
177
+ files.push(...collectSourceFiles(full, maxDepth, depth + 1));
178
+ }
179
+ else if (/\.(ts|js|mjs|cjs)$/.test(entry.name)) {
180
+ files.push(full);
181
+ }
182
+ }
183
+ }
184
+ catch { /* skip */ }
185
+ return files;
186
+ }
@@ -0,0 +1,2 @@
1
+ import type { ShellWardConfig } from '../types.js';
2
+ export declare function registerSecurityCommand(api: any, config: ShellWardConfig): void;