shellward 0.5.16 → 0.6.1

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 (41) hide show
  1. package/README.md +95 -30
  2. package/dist/auto-check.d.ts +1 -0
  3. package/dist/auto-check.js +12 -1
  4. package/dist/commands/index.d.ts +2 -1
  5. package/dist/commands/index.js +7 -0
  6. package/dist/commands/scan-mcp.d.ts +2 -0
  7. package/dist/commands/scan-mcp.js +105 -0
  8. package/dist/core/engine.d.ts +35 -0
  9. package/dist/core/engine.js +255 -33
  10. package/dist/index.d.ts +4 -2
  11. package/dist/index.js +18 -3
  12. package/dist/mcp-baseline.d.ts +27 -0
  13. package/dist/mcp-baseline.js +73 -0
  14. package/dist/mcp-client.d.ts +29 -0
  15. package/dist/mcp-client.js +264 -0
  16. package/dist/mcp-server.js +64 -9
  17. package/dist/rules/dangerous-commands.js +6 -2
  18. package/dist/rules/injection-en.js +27 -2
  19. package/dist/rules/injection-zh.js +27 -4
  20. package/dist/rules/sensitive-patterns.d.ts +13 -1
  21. package/dist/rules/sensitive-patterns.js +32 -5
  22. package/dist/rules/tool-poisoning.d.ts +8 -0
  23. package/dist/rules/tool-poisoning.js +96 -0
  24. package/dist/types.d.ts +32 -0
  25. package/dist/types.js +3 -1
  26. package/package.json +4 -2
  27. package/server.json +2 -2
  28. package/src/auto-check.ts +11 -1
  29. package/src/commands/index.ts +9 -1
  30. package/src/commands/scan-mcp.ts +118 -0
  31. package/src/core/engine.ts +273 -34
  32. package/src/index.ts +25 -5
  33. package/src/mcp-baseline.ts +97 -0
  34. package/src/mcp-client.ts +268 -0
  35. package/src/mcp-server.ts +71 -9
  36. package/src/rules/dangerous-commands.ts +6 -2
  37. package/src/rules/injection-en.ts +27 -2
  38. package/src/rules/injection-zh.ts +27 -4
  39. package/src/rules/sensitive-patterns.ts +37 -5
  40. package/src/rules/tool-poisoning.ts +108 -0
  41. package/src/types.ts +38 -1
@@ -0,0 +1,96 @@
1
+ // src/rules/tool-poisoning.ts — MCP tool-poisoning detection rules
2
+ //
3
+ // Tool poisoning = malicious instructions hidden in an MCP tool's *metadata*
4
+ // (description / parameter descriptions) that the LLM reads but the human never
5
+ // sees in the UI. These are distinct from generic prompt-injection in user text:
6
+ // they target the agent at tool-discovery time. Patterns below are tuned for the
7
+ // common public PoCs (Invariant Labs, MCP-Shield, Snyk agent-scan).
8
+ export const TOOL_POISONING_RULES = [
9
+ // ===== Hidden instruction markup =====
10
+ {
11
+ id: 'tp_important_tag',
12
+ name: 'Hidden <IMPORTANT>/<system> directive in description',
13
+ pattern: /<\s*(?:important|system|secret|instructions?|admin)\s*>/i,
14
+ riskScore: 45,
15
+ category: 'hidden_instruction',
16
+ },
17
+ {
18
+ id: 'tp_before_using',
19
+ name: 'Pre-tool instruction injection',
20
+ pattern: /before\s+(?:using|calling|invoking|running)\s+(?:any\s+other|this|the|another)\s+tool/i,
21
+ riskScore: 40,
22
+ category: 'hidden_instruction',
23
+ },
24
+ {
25
+ id: 'tp_zh_before_using',
26
+ name: '工具描述内前置指令注入',
27
+ pattern: /(?:在使用|调用|执行)(?:任何)?(?:其他|这个|该)?工具(?:之前|前)/,
28
+ riskScore: 40,
29
+ category: 'hidden_instruction',
30
+ },
31
+ // ===== Concealment ("don't tell the user") =====
32
+ {
33
+ id: 'tp_do_not_tell',
34
+ name: 'Instruction to hide activity from user',
35
+ pattern: /(?:do\s+not|don'?t|never)\s+(?:tell|inform|mention|notify|reveal|show)\s+(?:to\s+)?(?:the\s+)?(?:user|human|operator)/i,
36
+ riskScore: 45,
37
+ category: 'concealment',
38
+ },
39
+ {
40
+ id: 'tp_zh_do_not_tell',
41
+ name: '指示对用户隐藏行为',
42
+ pattern: /(?:不要|不得|切勿|别)(?:告诉|告知|提示|通知|让)?(?:用户|使用者)(?:知道|看到|发现)?/,
43
+ riskScore: 45,
44
+ category: 'concealment',
45
+ },
46
+ {
47
+ id: 'tp_without_user',
48
+ name: 'Act without user knowledge/consent',
49
+ pattern: /without\s+(?:the\s+)?(?:user'?s?\s+)?(?:knowledge|consent|awareness|noticing|telling)/i,
50
+ riskScore: 40,
51
+ category: 'concealment',
52
+ },
53
+ // ===== Sensitive data access from a tool description =====
54
+ {
55
+ id: 'tp_read_secrets',
56
+ // Bare mention of a sensitive path is only weakly suspicious — legitimate
57
+ // tools (dotenv loaders, ssh managers) and security tools name these too.
58
+ // Scored below threshold so it must corroborate another signal to block.
59
+ name: 'Description references sensitive files',
60
+ pattern: /(?:~\/\.ssh|id_rsa|\.aws\/credentials|\.env\b|\.cursor\/mcp\.json|\.npmrc|\/etc\/passwd|\.config\/.*(?:token|secret|credential))/i,
61
+ riskScore: 25,
62
+ category: 'data_access',
63
+ },
64
+ {
65
+ id: 'tp_pass_file_contents',
66
+ name: 'Description asks to pass file/secret contents as a parameter',
67
+ pattern: /(?:pass|include|read|send|provide|attach)\s+(?:the\s+)?(?:full\s+)?(?:contents?|content|value)\s+of\s+(?:the\s+)?(?:file|\S*(?:key|token|secret|password|credential))/i,
68
+ riskScore: 35,
69
+ category: 'data_access',
70
+ },
71
+ // ===== Exfiltration hints =====
72
+ {
73
+ id: 'tp_exfil_url',
74
+ name: 'Description instructs sending data to a URL',
75
+ pattern: /(?:send|transmit|upload|post|exfiltrate|forward)\s+(?:it|this|the\s+\w+|data|results?)?\s*(?:to|via)\s+(?:https?:\/\/|the\s+(?:webhook|endpoint|server|url))/i,
76
+ riskScore: 40,
77
+ category: 'exfiltration',
78
+ },
79
+ {
80
+ id: 'tp_exfiltrate_verb',
81
+ // "exfiltrate" in a tool description is almost never benign.
82
+ name: 'Exfiltration verb in description',
83
+ pattern: /\bexfiltrat(?:e|ion|ing)\b/i,
84
+ riskScore: 35,
85
+ category: 'exfiltration',
86
+ },
87
+ {
88
+ id: 'tp_sidechannel',
89
+ // A bare side-channel hostname is weak on its own (could be documentation);
90
+ // scored below threshold so it must accompany another signal to block.
91
+ name: 'Known exfiltration side-channel keyword',
92
+ pattern: /\b(?:webhook\.site|requestbin|pastebin|ngrok\.io|burpcollaborator|interact\.sh|oast\b)/i,
93
+ riskScore: 25,
94
+ category: 'exfiltration',
95
+ },
96
+ ];
package/dist/types.d.ts CHANGED
@@ -14,6 +14,38 @@ export interface ShellWardConfig {
14
14
  sessionGuard: boolean;
15
15
  };
16
16
  injectionThreshold: number;
17
+ /** User-supplied rules merged on top of the built-ins (additive; allowedTools wins). */
18
+ customRules?: CustomRules;
19
+ }
20
+ /** A user-defined PII/secret pattern (regex source as a string for JSON-friendliness). */
21
+ export interface CustomSensitivePattern {
22
+ id: string;
23
+ name: string;
24
+ pattern: string;
25
+ flags?: string;
26
+ replacement?: string;
27
+ }
28
+ /** A user-defined dangerous-command pattern. */
29
+ export interface CustomCommandRule {
30
+ id: string;
31
+ pattern: string;
32
+ flags?: string;
33
+ description?: string;
34
+ }
35
+ /**
36
+ * Extension points for any platform embedding ShellWard. All fields are optional
37
+ * and ADDITIVE on top of the built-in rules — except `allowedTools`, which always
38
+ * wins (a tool listed there is never blocked and is treated as low-risk).
39
+ */
40
+ export interface CustomRules {
41
+ blockedTools?: string[];
42
+ allowedTools?: string[];
43
+ sensitiveTools?: string[];
44
+ outboundTools?: string[];
45
+ honeypotPaths?: string[];
46
+ sensitivePatterns?: CustomSensitivePattern[];
47
+ dangerousCommands?: CustomCommandRule[];
48
+ injectionRules?: InjectionRule[];
17
49
  }
18
50
  export type ResolvedLocale = 'zh' | 'en';
19
51
  export interface AuditEntry {
package/dist/types.js CHANGED
@@ -13,7 +13,9 @@ export const DEFAULT_CONFIG = {
13
13
  dataFlowGuard: true,
14
14
  sessionGuard: true,
15
15
  },
16
- injectionThreshold: 60,
16
+ // 40 catches single high-confidence signals (one strong rule = a block) while
17
+ // keeping benign "act as…"-style phrasing (≤35) safe. Calibrated against bench/.
18
+ injectionThreshold: 40,
17
19
  };
18
20
  /**
19
21
  * Detect locale from system environment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.5.16",
3
+ "version": "0.6.1",
4
4
  "mcpName": "io.github.jnMetaCode/shellward",
5
5
  "description": "AI agent security & MCP security middleware — prompt injection detection, AI firewall, runtime guardrails & data-loss prevention for LLM tool calls. 8-layer defense against data exfiltration & dangerous commands. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents, Hermes Agent.",
6
6
  "keywords": [
@@ -57,11 +57,13 @@
57
57
  "scripts": {
58
58
  "build": "tsc",
59
59
  "mcp": "npx tsx src/mcp-server.ts",
60
- "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-mcp.ts",
60
+ "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-rugpull.ts && npx tsx test-redos.ts && npx tsx test-mcp-client.ts && npx tsx test-mcp.ts",
61
+ "test:redos": "npx tsx test-redos.ts",
61
62
  "test:integration": "npx tsx test-integration.ts",
62
63
  "test:edge": "npx tsx test-edge-cases.ts",
63
64
  "test:sdk": "npx tsx test-sdk.ts",
64
65
  "test:mcp": "npx tsx test-mcp.ts",
66
+ "bench": "npx tsx bench/run.ts",
65
67
  "prepublishOnly": "npm run build"
66
68
  },
67
69
  "openclaw": {
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/jnMetaCode/shellward",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.5.15",
9
+ "version": "0.6.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "shellward",
14
- "version": "0.5.15",
14
+ "version": "0.6.1",
15
15
  "runtime": "node",
16
16
  "transport": {
17
17
  "type": "stdio"
package/src/auto-check.ts CHANGED
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs'
6
6
  import { join } from 'path'
7
7
  import { getHomeDir } from './utils.js'
8
8
  import { fetchVulnDB, compareVersions } from './update-check.js'
9
+ import { discoverMcpServers } from './mcp-client.js'
9
10
 
10
11
  const OPENCLAW_DIR = join(getHomeDir(), '.openclaw')
11
12
 
@@ -13,6 +14,7 @@ export interface AutoCheckResult {
13
14
  openclawVulns: { id: string; severity: string; description: string }[]
14
15
  pluginRisks: { plugin: string; risk: string }[]
15
16
  mcpRisks: { config: string; risk: string }[]
17
+ mcpServerCount: number
16
18
  rootWarning: boolean
17
19
  }
18
20
 
@@ -131,7 +133,9 @@ export async function runAutoCheck(locale: 'zh' | 'en' = 'en'): Promise<AutoChec
131
133
  Promise.resolve(scanMcpConfig()),
132
134
  ])
133
135
  const rootWarning = typeof process.getuid === 'function' && process.getuid() === 0
134
- return { openclawVulns, pluginRisks, mcpRisks, rootWarning }
136
+ let mcpServerCount = 0
137
+ try { mcpServerCount = discoverMcpServers().filter(s => s.transport === 'stdio').length } catch { /* ignore */ }
138
+ return { openclawVulns, pluginRisks, mcpRisks, mcpServerCount, rootWarning }
135
139
  }
136
140
 
137
141
  /**
@@ -172,6 +176,12 @@ export function runAutoCheckOnStartup(logger: { warn: (s: string) => void }, loc
172
176
  lines.push(zh ? '⚠️ 正在以 root 运行,建议使用普通用户' : '⚠️ Running as root, consider using non-root user')
173
177
  }
174
178
 
179
+ if (result.mcpServerCount > 0) {
180
+ lines.push(zh
181
+ ? `🔌 检测到 ${result.mcpServerCount} 个 MCP 服务器 — 运行 /scan-mcp 检测工具投毒与 rug-pull`
182
+ : `🔌 ${result.mcpServerCount} MCP server(s) configured — run /scan-mcp to check for tool poisoning & rug-pulls`)
183
+ }
184
+
175
185
  if (lines.length > 0) {
176
186
  logger.warn((zh ? '[ShellWard] 自动安全检查:\n' : '[ShellWard] Auto security check:\n') + lines.join('\n'))
177
187
  }
@@ -6,10 +6,12 @@ import { registerSecurityCommand } from './security.js'
6
6
  import { registerAuditCommand } from './audit.js'
7
7
  import { registerHardenCommand } from './harden.js'
8
8
  import { registerScanPluginsCommand } from './scan-plugins.js'
9
+ import { registerScanMcpCommand } from './scan-mcp.js'
9
10
  import { registerCheckUpdatesCommand } from './check-updates.js'
10
11
  import { registerUpgradeOpenClawCommand } from './upgrade-openclaw.js'
11
12
 
12
- export function registerAllCommands(api: any, config: ShellWardConfig) {
13
+ /** @returns number of registered commands (for the startup log). */
14
+ export function registerAllCommands(api: any, config: ShellWardConfig): number {
13
15
  const locale = resolveLocale(config)
14
16
 
15
17
  // Register individual commands
@@ -17,6 +19,7 @@ export function registerAllCommands(api: any, config: ShellWardConfig) {
17
19
  registerAuditCommand(api, config)
18
20
  registerHardenCommand(api, config)
19
21
  registerScanPluginsCommand(api, config)
22
+ registerScanMcpCommand(api, config)
20
23
  registerCheckUpdatesCommand(api, config)
21
24
  registerUpgradeOpenClawCommand(api, config)
22
25
 
@@ -36,6 +39,7 @@ export function registerAllCommands(api: any, config: ShellWardConfig) {
36
39
  | \`/audit [数量] [过滤]\` | 查看审计日志 (过滤: block/audit/critical/high) |
37
40
  | \`/harden\` | 安全扫描 · \`/harden fix\` 自动修复权限 |
38
41
  | \`/scan-plugins\` | 扫描已安装插件的安全风险 |
42
+ | \`/scan-mcp\` | 扫描已配置 MCP 服务器(工具投毒 + rug-pull) |
39
43
  | \`/check-updates\` | 检查 OpenClaw 版本和已知漏洞 |
40
44
  | \`/upgrade-openclaw\` | 一键升级 OpenClaw · \`/upgrade-openclaw yes\` 直接执行 |
41
45
 
@@ -50,6 +54,7 @@ L5 安全门 · L6 回复审计 · L7 数据流监控 · L8 会话安全`
50
54
  | \`/audit [count] [filter]\` | View audit log (filter: block/audit/critical/high) |
51
55
  | \`/harden\` | Security scan · \`/harden fix\` to auto-fix permissions |
52
56
  | \`/scan-plugins\` | Scan installed plugins for security risks |
57
+ | \`/scan-mcp\` | Scan configured MCP servers (tool poisoning + rug-pull) |
53
58
  | \`/check-updates\` | Check OpenClaw version and known vulnerabilities |
54
59
  | \`/upgrade-openclaw\` | Upgrade OpenClaw · \`/upgrade-openclaw yes\` to execute |
55
60
 
@@ -58,4 +63,7 @@ L1 Prompt Guard · L2 Output Scanner · L3 Tool Blocker · L4 Input Auditor
58
63
  L5 Security Gate · L6 Outbound Guard · L7 Data Flow Guard · L8 Session Guard`,
59
64
  }),
60
65
  })
66
+
67
+ // 7 individual commands + /cg help
68
+ return 8
61
69
  }
@@ -0,0 +1,118 @@
1
+ // src/commands/scan-mcp.ts — /scan-mcp: connect to configured MCP servers and
2
+ // scan their tool definitions for poisoning + rug-pulls.
3
+ //
4
+ // Safety model (mirrors Snyk agent-scan): scanning spawns the configured stdio
5
+ // servers, so it is an explicit user action — never auto-run at startup.
6
+
7
+ import { ShellWard } from '../core/engine.js'
8
+ import { McpBaseline } from '../mcp-baseline.js'
9
+ import { discoverMcpServers, listToolsStdio, listToolsHttp } from '../mcp-client.js'
10
+ import type { ShellWardConfig } from '../types.js'
11
+ import { resolveLocale } from '../types.js'
12
+
13
+ export function registerScanMcpCommand(api: any, config: ShellWardConfig) {
14
+ const locale = resolveLocale(config)
15
+
16
+ api.registerCommand({
17
+ name: 'scan-mcp',
18
+ description: locale === 'zh'
19
+ ? '🔌 扫描已配置的 MCP 服务器(工具投毒 + rug-pull 检测)'
20
+ : '🔌 Scan configured MCP servers (tool poisoning + rug-pull)',
21
+ acceptsArgs: false,
22
+ handler: async () => {
23
+ const zh = locale === 'zh'
24
+ const guard = new ShellWard(config)
25
+ const baseline = new McpBaseline()
26
+ const lines: string[] = []
27
+
28
+ lines.push(zh ? '🔌 **MCP 服务器安全扫描**' : '🔌 **MCP Server Security Scan**')
29
+ lines.push('')
30
+
31
+ const servers = discoverMcpServers()
32
+ if (servers.length === 0) {
33
+ lines.push(zh
34
+ ? 'ℹ️ 未发现已配置的 MCP 服务器(检查 ~/.openclaw/mcp.json 等)。'
35
+ : 'ℹ️ No configured MCP servers found (checked ~/.openclaw/mcp.json etc).')
36
+ return { text: lines.join('\n') }
37
+ }
38
+
39
+ const stdioServers = servers.filter(s => s.transport === 'stdio')
40
+ const remoteServers = servers.filter(s => s.transport === 'remote')
41
+
42
+ lines.push(zh
43
+ ? `发现 ${servers.length} 个服务器(${stdioServers.length} 个 stdio,${remoteServers.length} 个远程)`
44
+ : `Found ${servers.length} servers (${stdioServers.length} stdio, ${remoteServers.length} remote)`)
45
+ lines.push('')
46
+
47
+ let totalTools = 0
48
+ let poisoned = 0
49
+ let rugPulls = 0
50
+ let unreachable = 0
51
+
52
+ for (const server of servers) {
53
+ let tools
54
+ try {
55
+ tools = server.transport === 'remote'
56
+ ? await listToolsHttp(server)
57
+ : await listToolsStdio(server)
58
+ } catch (e: any) {
59
+ unreachable++
60
+ const where = server.transport === 'remote' ? server.url || 'remote' : 'stdio'
61
+ lines.push(zh
62
+ ? `### ⚠️ ${server.name} (${where}) — 无法连接 (${e?.message || 'error'})`
63
+ : `### ⚠️ ${server.name} (${where}) — unreachable (${e?.message || 'error'})`)
64
+ lines.push('')
65
+ continue
66
+ }
67
+
68
+ const serverIssues: string[] = []
69
+ for (const tool of tools) {
70
+ totalTools++
71
+ const scan = guard.scanToolDefinition(tool)
72
+ const rp = baseline.record(McpBaseline.keyFor(server.name, tool.name), tool)
73
+
74
+ if (!scan.safe) {
75
+ poisoned++
76
+ serverIssues.push(zh
77
+ ? ` 🔴 \`${tool.name}\` 工具投毒 (评分 ${scan.score}): ${scan.findings.map(f => f.name).join('; ')}`
78
+ : ` 🔴 \`${tool.name}\` poisoned (score ${scan.score}): ${scan.findings.map(f => f.name).join('; ')}`)
79
+ }
80
+ if (rp.status === 'changed') {
81
+ rugPulls++
82
+ serverIssues.push(zh
83
+ ? ` 🟠 \`${tool.name}\` 描述自上次以来被修改 (rug-pull 嫌疑)`
84
+ : ` 🟠 \`${tool.name}\` description changed since last seen (possible rug-pull)`)
85
+ }
86
+ }
87
+
88
+ const icon = serverIssues.length > 0 ? '🔴' : '✅'
89
+ const tag = server.transport === 'remote' ? ' (remote)' : ''
90
+ lines.push(`### ${icon} ${server.name}${tag}`)
91
+ lines.push(zh ? ` ${tools.length} 个工具` : ` ${tools.length} tools`)
92
+ if (serverIssues.length === 0) {
93
+ lines.push(zh ? ' ✅ 未发现问题' : ' ✅ No issues found')
94
+ } else {
95
+ lines.push(...serverIssues)
96
+ }
97
+ lines.push('')
98
+ }
99
+
100
+ baseline.save()
101
+
102
+ // Summary
103
+ lines.push('---')
104
+ lines.push(zh
105
+ ? `扫描了 ${totalTools} 个工具 · 🔴 投毒 ${poisoned} · 🟠 rug-pull ${rugPulls} · ⚠️ 无法连接 ${unreachable}`
106
+ : `Scanned ${totalTools} tools · 🔴 poisoned ${poisoned} · 🟠 rug-pull ${rugPulls} · ⚠️ unreachable ${unreachable}`)
107
+ if (poisoned === 0 && rugPulls === 0) {
108
+ lines.push(zh ? '✅ **所有 MCP 工具通过扫描**' : '✅ **All MCP tools passed**')
109
+ } else {
110
+ lines.push(zh
111
+ ? '⚠️ **发现可疑 MCP 工具 — 请审查或移除对应服务器**'
112
+ : '⚠️ **Suspicious MCP tools found — review or remove the server**')
113
+ }
114
+
115
+ return { text: lines.join('\n') }
116
+ },
117
+ })
118
+ }