clawmoat 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CONTRIBUTING.md +4 -2
  4. package/DEMO.md +87 -0
  5. package/Dockerfile +5 -18
  6. package/README.md +294 -8
  7. package/SECURITY.md +58 -10
  8. package/THREAT_MODEL.md +129 -0
  9. package/agent/README.md +131 -0
  10. package/agent/index.js +471 -0
  11. package/agent/install-service.sh +94 -0
  12. package/agent/openclaw-hook.js +453 -0
  13. package/agent/provider-setup.js +649 -0
  14. package/agent/setup.js +274 -0
  15. package/assets/BADGE-USAGE.md +20 -0
  16. package/assets/clawmoat-badge.svg +21 -0
  17. package/bin/clawmoat.js +468 -111
  18. package/docs/affiliates/dashboard.html +124 -0
  19. package/docs/affiliates/index.html +236 -0
  20. package/docs/agent-install.html +183 -0
  21. package/docs/ai-agent-security-scanner.html +10 -6
  22. package/docs/badge/index.html +149 -0
  23. package/docs/badge/scanning.svg +23 -0
  24. package/docs/blog/386-malicious-skills.html +262 -0
  25. package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
  26. package/docs/blog/agent-trust-protocol.html +198 -0
  27. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  28. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  29. package/docs/blog/calculator-math.html +180 -0
  30. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +229 -0
  31. package/docs/blog/host-guardian-launch.html +18 -8
  32. package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
  33. package/docs/blog/index.html +211 -9
  34. package/docs/blog/langchain-security-tutorial.html +18 -8
  35. package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
  36. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  37. package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
  38. package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
  39. package/docs/blog/oasis-websocket-hijack.html +212 -0
  40. package/docs/blog/ollama-openclaw-security.html +160 -0
  41. package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
  42. package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
  43. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  44. package/docs/blog/securing-ai-agents.html +18 -8
  45. package/docs/blog/supply-chain-agents.html +18 -8
  46. package/docs/business/index.html +525 -0
  47. package/docs/business/install.html +261 -0
  48. package/docs/checklist.html +174 -0
  49. package/docs/compare/index.html +122 -0
  50. package/docs/compare/lakera/index.html +62 -0
  51. package/docs/compare/llm-guard/index.html +49 -0
  52. package/docs/compare/snyk-agent-scan/index.html +63 -0
  53. package/docs/compare.html +10 -6
  54. package/docs/dashboard/index.html +520 -0
  55. package/docs/finance/index.html +220 -0
  56. package/docs/guides/business-deployment.html +770 -0
  57. package/docs/hall-of-fame.html +174 -0
  58. package/docs/index.html +447 -154
  59. package/docs/install.sh +557 -0
  60. package/docs/integrations/langchain.html +14 -6
  61. package/docs/integrations/openai.html +14 -6
  62. package/docs/integrations/openclaw.html +55 -7
  63. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  64. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  65. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  66. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  67. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  68. package/docs/plans/2026-05-12-sales-push.md +303 -0
  69. package/docs/playground/index.html +893 -0
  70. package/docs/playground.html +4 -7
  71. package/docs/privacy-policy/index.html +122 -0
  72. package/docs/rfcs/defense-in-depth.md +467 -0
  73. package/docs/scan/index.html +358 -0
  74. package/docs/services/case-study.html +255 -0
  75. package/docs/services/downloads/install-openclaw.bat +45 -0
  76. package/docs/services/downloads/install-openclaw.command +38 -0
  77. package/docs/services/downloads/install-openclaw.sh +38 -0
  78. package/docs/services/get-started.html +165 -0
  79. package/docs/services/index.html +598 -0
  80. package/docs/services/multi-agent-security.html +284 -0
  81. package/docs/services/one-pager.html +99 -0
  82. package/docs/services/pitch-deck.html +229 -0
  83. package/docs/services/roi-calculator.html +258 -0
  84. package/docs/sitemap.xml +192 -2
  85. package/docs/support/index.html +135 -0
  86. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  87. package/docs/templates/customer-service/MEMORY.md +89 -0
  88. package/docs/templates/customer-service/SOUL.md +41 -0
  89. package/docs/templates/customer-service/USER.md +56 -0
  90. package/docs/templates/executive/HEARTBEAT.md +86 -0
  91. package/docs/templates/executive/MEMORY.md +92 -0
  92. package/docs/templates/executive/SOUL.md +44 -0
  93. package/docs/templates/executive/USER.md +62 -0
  94. package/docs/templates/finance/HEARTBEAT.md +58 -0
  95. package/docs/templates/finance/MEMORY.md +87 -0
  96. package/docs/templates/finance/SOUL.md +38 -0
  97. package/docs/templates/finance/USER.md +53 -0
  98. package/docs/templates/index.html +115 -0
  99. package/docs/templates/operations/HEARTBEAT.md +63 -0
  100. package/docs/templates/operations/MEMORY.md +68 -0
  101. package/docs/templates/operations/SOUL.md +38 -0
  102. package/docs/templates/operations/USER.md +49 -0
  103. package/docs/templates/sales/HEARTBEAT.md +55 -0
  104. package/docs/templates/sales/MEMORY.md +89 -0
  105. package/docs/templates/sales/SOUL.md +34 -0
  106. package/docs/templates/sales/USER.md +54 -0
  107. package/docs/terms-of-service/index.html +122 -0
  108. package/eslint.config.js +32 -0
  109. package/evals/README.md +29 -0
  110. package/evals/cases.json +390 -0
  111. package/evals/results.md +68 -0
  112. package/evals/run.js +180 -0
  113. package/examples/basic-usage.js +38 -0
  114. package/examples/demo-attack/demo.js +186 -0
  115. package/examples/python-quickstart/README.md +54 -0
  116. package/examples/python-quickstart/clawmoat_client.py +167 -0
  117. package/examples/video-demo/README.md +14 -0
  118. package/examples/video-demo/scene-a-normal.js +29 -0
  119. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  120. package/examples/video-demo/scene-c-hijack.js +44 -0
  121. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  122. package/integrations/crewai/README.md +32 -0
  123. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  124. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  125. package/integrations/crewai/pyproject.toml +21 -0
  126. package/integrations/langchain/README.md +91 -0
  127. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  128. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  129. package/integrations/langchain/pyproject.toml +32 -0
  130. package/integrations/litellm/README.md +324 -0
  131. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  132. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  133. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  134. package/integrations/litellm/pyproject.toml +74 -0
  135. package/integrations/openai-agents/README.md +392 -0
  136. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  137. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  138. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  139. package/integrations/openai-agents/pyproject.toml +76 -0
  140. package/package.json +6 -5
  141. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  142. package/plugins/openclaw-adapter/README.md +103 -0
  143. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  144. package/plugins/openclaw-adapter/package.json +31 -0
  145. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  146. package/plugins/openclaw-adapter/src/index.ts +140 -0
  147. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  148. package/server/data/threats.json +290 -0
  149. package/server/index.js +224 -10
  150. package/src/adapters/express.js +161 -0
  151. package/src/adapters/index.js +92 -0
  152. package/src/adapters/langchain.js +185 -0
  153. package/src/approval/index.js +456 -0
  154. package/src/ban-scanner.js +200 -0
  155. package/src/boundary-scanner.js +296 -0
  156. package/src/ci-scanner.js +279 -0
  157. package/src/code-scanner.js +245 -0
  158. package/src/enforce.js +166 -0
  159. package/src/finance/index.js +585 -0
  160. package/src/finance/mcp-firewall.js +486 -0
  161. package/src/formatters/json.js +80 -0
  162. package/src/formatters/sarif.js +388 -0
  163. package/src/guardian/alerts.js +34 -3
  164. package/src/guardian/gateway-monitor.js +590 -0
  165. package/src/guardian/index.js +41 -2
  166. package/src/index.js +105 -0
  167. package/src/integrations/agentmesh.js +501 -0
  168. package/src/language-detector.js +201 -0
  169. package/src/mcp-scanner.js +253 -0
  170. package/src/multimodal/index.js +579 -0
  171. package/src/obfuscation-scanner.js +457 -0
  172. package/src/policy-engine.js +402 -0
  173. package/src/scanners/dependency-attacks.js +128 -0
  174. package/src/scanners/prompt-injection.js +18 -0
  175. package/src/scanners/supply-chain.js +14 -0
  176. package/src/templates/default-config.yml +90 -0
  177. package/src/vuln-ops/exploitability.js +46 -0
  178. package/src/watch/live-monitor.js +720 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * ClawMoat Policy Engine — Declarative security rules for AI agents
3
+ *
4
+ * Define rules in YAML/JSON, enforce them at runtime.
5
+ * This is what makes ClawMoat a firewall, not a scanner.
6
+ *
7
+ * @example
8
+ * const { PolicyEngine } = require('clawmoat');
9
+ * const engine = new PolicyEngine('./clawmoat-policy.yaml');
10
+ *
11
+ * // Evaluate a tool call
12
+ * const decision = engine.evaluate({
13
+ * type: 'tool_call',
14
+ * tool: 'slack.send',
15
+ * args: { channel: '#general', text: 'API key: sk-...' },
16
+ * context: { user: 'agent-1', source: 'mcp' }
17
+ * });
18
+ *
19
+ * if (decision.action === 'block') {
20
+ * console.log('Blocked:', decision.reason);
21
+ * }
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ // Severity levels (ordered)
28
+ const SEVERITY_ORDER = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
29
+
30
+ // Built-in data patterns for classification
31
+ const DATA_PATTERNS = {
32
+ secret: [
33
+ /sk-(?:proj-)?[a-zA-Z0-9]{20,}/g, // OpenAI keys
34
+ /ghp_[a-zA-Z0-9]{36}/g, // GitHub PAT
35
+ /glpat-[a-zA-Z0-9\-_]{20,}/g, // GitLab PAT
36
+ /xox[bpsa]-[a-zA-Z0-9\-]{10,}/g, // Slack tokens
37
+ /AKIA[A-Z0-9]{16}/g, // AWS access key
38
+ /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, // Private keys
39
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, // JWT tokens
40
+ ],
41
+ pii: [
42
+ /\b\d{3}-\d{2}-\d{4}\b/g, // SSN
43
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, // Email
44
+ /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, // Credit card
45
+ /\b\d{3}[\s.-]?\d{3}[\s.-]?\d{4}\b/g, // Phone number
46
+ ],
47
+ internal: [
48
+ /(?:internal|confidential|restricted|do not share)/gi,
49
+ /(?:JIRA|CONFLUENCE)-[A-Z]+-\d+/g, // Internal ticket refs
50
+ ],
51
+ };
52
+
53
+ // Built-in tool risk levels
54
+ const TOOL_RISK_DEFAULTS = {
55
+ // Read-only = low
56
+ 'read': 'low', 'get': 'low', 'list': 'low', 'search': 'low', 'query': 'low',
57
+ // Side-effecting = medium
58
+ 'create': 'medium', 'update': 'medium', 'write': 'medium', 'put': 'medium',
59
+ // External comms = high
60
+ 'send': 'high', 'email': 'high', 'post': 'high', 'publish': 'high', 'notify': 'high',
61
+ // Destructive/privileged = critical
62
+ 'delete': 'critical', 'exec': 'critical', 'shell': 'critical', 'eval': 'critical',
63
+ 'drop': 'critical', 'rm': 'critical', 'chmod': 'critical', 'sudo': 'critical',
64
+ };
65
+
66
+ /**
67
+ * @typedef {Object} PolicyRule
68
+ * @property {string} id - Unique rule identifier
69
+ * @property {string} [description] - Human-readable description
70
+ * @property {string} [severity] - Rule severity (critical/high/medium/low)
71
+ * @property {Object} when - Match conditions
72
+ * @property {string} action - Enforcement action (block/warn/sanitize/log/require_approval)
73
+ * @property {string} [message] - Message to include in decision
74
+ */
75
+
76
+ /**
77
+ * @typedef {Object} EvalEvent
78
+ * @property {string} type - Event type (tool_call/inbound/outbound/memory_write/retrieval)
79
+ * @property {string} [tool] - Tool name (for tool_call events)
80
+ * @property {Object} [args] - Tool arguments
81
+ * @property {string} [text] - Text content (for inbound/outbound)
82
+ * @property {Object} [context] - Additional context (user, source, session, etc.)
83
+ */
84
+
85
+ /**
86
+ * @typedef {Object} Decision
87
+ * @property {string} action - Final action (allow/block/warn/sanitize/require_approval)
88
+ * @property {string} [ruleId] - ID of the rule that triggered
89
+ * @property {string} [reason] - Human-readable reason
90
+ * @property {string} [severity] - Severity of the match
91
+ * @property {Array} matchedRules - All rules that matched
92
+ * @property {Object} classifications - Data classifications found
93
+ * @property {number} timestamp - Unix timestamp
94
+ * @property {number} latencyMs - Processing time
95
+ */
96
+
97
+ class PolicyEngine {
98
+ /**
99
+ * @param {string|Object|Array} policy - Path to YAML/JSON file, or policy object/array
100
+ * @param {Object} [options]
101
+ * @param {string} [options.mode='block'] - Default mode: monitor/warn/sanitize/block
102
+ * @param {boolean} [options.trace=true] - Enable execution tracing
103
+ * @param {Function} [options.onDecision] - Callback for every decision
104
+ */
105
+ constructor(policy, options = {}) {
106
+ this.mode = options.mode || 'block';
107
+ this.trace = options.trace !== false;
108
+ this.onDecision = options.onDecision || null;
109
+ this.rules = [];
110
+ this.traceLog = [];
111
+ this.stats = { total: 0, allowed: 0, blocked: 0, warned: 0, sanitized: 0 };
112
+ this.toolRiskOverrides = {};
113
+
114
+ if (typeof policy === 'string') {
115
+ this._loadFromFile(policy);
116
+ } else if (Array.isArray(policy)) {
117
+ this.rules = policy;
118
+ } else if (policy && policy.rules) {
119
+ this.rules = policy.rules;
120
+ if (policy.tool_risks) this.toolRiskOverrides = policy.tool_risks;
121
+ if (policy.mode) this.mode = policy.mode;
122
+ }
123
+ }
124
+
125
+ _loadFromFile(filePath) {
126
+ const ext = path.extname(filePath);
127
+ const raw = fs.readFileSync(filePath, 'utf8');
128
+
129
+ if (ext === '.json') {
130
+ const parsed = JSON.parse(raw);
131
+ this.rules = parsed.rules || parsed;
132
+ if (parsed.tool_risks) this.toolRiskOverrides = parsed.tool_risks;
133
+ if (parsed.mode) this.mode = parsed.mode;
134
+ } else if (ext === '.yaml' || ext === '.yml') {
135
+ // Simple YAML parser (no deps) — handles the subset we need
136
+ const parsed = this._parseSimpleYaml(raw);
137
+ this.rules = parsed.rules || [];
138
+ if (parsed.tool_risks) this.toolRiskOverrides = parsed.tool_risks;
139
+ if (parsed.mode) this.mode = parsed.mode;
140
+ } else {
141
+ throw new Error(`Unsupported policy file format: ${ext}. Use .json or .yaml`);
142
+ }
143
+ }
144
+
145
+ _parseSimpleYaml(text) {
146
+ // Minimal YAML parser for policy files — handles maps, arrays, strings
147
+ // For production, users should convert to JSON. This handles 90% of cases.
148
+ try {
149
+ // Strip comments
150
+ const lines = text.split('\n').filter(l => !l.trim().startsWith('#'));
151
+ const cleaned = lines.join('\n');
152
+ // Convert YAML-ish to JSON-ish (basic)
153
+ let json = cleaned
154
+ .replace(/:\s*\n/g, ': null\n') // empty values
155
+ .replace(/^(\s*)- /gm, '$1 ') // arrays
156
+ ;
157
+ // Fall back to JSON.parse if it looks like JSON
158
+ if (cleaned.trim().startsWith('{') || cleaned.trim().startsWith('[')) {
159
+ return JSON.parse(cleaned);
160
+ }
161
+ // For complex YAML, tell user to use JSON
162
+ throw new Error('Complex YAML detected. Please use JSON format for policy files, or install a YAML parser.');
163
+ } catch(e) {
164
+ throw new Error(`Failed to parse policy YAML: ${e.message}. Tip: use .json format for reliability.`);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Classify text content for data types
170
+ * @param {string} text
171
+ * @returns {Object} classifications { secret: [...], pii: [...], internal: [...] }
172
+ */
173
+ classify(text) {
174
+ if (!text || typeof text !== 'string') return {};
175
+ const result = {};
176
+ for (const [category, patterns] of Object.entries(DATA_PATTERNS)) {
177
+ const matches = [];
178
+ for (const pattern of patterns) {
179
+ const p = new RegExp(pattern.source, pattern.flags);
180
+ const found = text.match(p);
181
+ if (found) matches.push(...found);
182
+ }
183
+ if (matches.length > 0) result[category] = matches;
184
+ }
185
+ return result;
186
+ }
187
+
188
+ /**
189
+ * Get risk level for a tool
190
+ * @param {string} toolName
191
+ * @returns {string} risk level
192
+ */
193
+ getToolRisk(toolName) {
194
+ if (this.toolRiskOverrides[toolName]) return this.toolRiskOverrides[toolName];
195
+ const lower = (toolName || '').toLowerCase();
196
+ for (const [keyword, risk] of Object.entries(TOOL_RISK_DEFAULTS)) {
197
+ if (lower.includes(keyword)) return risk;
198
+ }
199
+ return 'medium'; // default
200
+ }
201
+
202
+ /**
203
+ * Check if an event matches a rule's conditions
204
+ * @param {EvalEvent} event
205
+ * @param {PolicyRule} rule
206
+ * @returns {boolean}
207
+ */
208
+ _matchRule(event, rule) {
209
+ const when = rule.when;
210
+ if (!when) return false;
211
+
212
+ // Match event type
213
+ if (when.type && when.type !== event.type) return false;
214
+
215
+ // Match tool name
216
+ if (when.tool) {
217
+ const tools = Array.isArray(when.tool) ? when.tool : [when.tool];
218
+ const eventTool = event.tool || '';
219
+ if (!tools.some(t => {
220
+ if (t === '*') return true;
221
+ if (t.includes('*')) {
222
+ // Convert glob to regex: "http.*" → /^http\..*$/
223
+ const re = new RegExp('^' + t.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
224
+ return re.test(eventTool);
225
+ }
226
+ return eventTool === t || eventTool.toLowerCase() === t.toLowerCase();
227
+ })) return false;
228
+ }
229
+
230
+ // Match text content contains
231
+ if (when.input_contains) {
232
+ const text = this._getEventText(event);
233
+ const terms = Array.isArray(when.input_contains) ? when.input_contains : [when.input_contains];
234
+ if (!terms.some(term => text.toLowerCase().includes(term.toLowerCase()))) return false;
235
+ }
236
+
237
+ // Match text content regex
238
+ if (when.input_matches) {
239
+ const text = this._getEventText(event);
240
+ const patterns = Array.isArray(when.input_matches) ? when.input_matches : [when.input_matches];
241
+ if (!patterns.some(p => new RegExp(p, 'i').test(text))) return false;
242
+ }
243
+
244
+ // Match data classification
245
+ if (when.data_classification) {
246
+ const text = this._getEventText(event);
247
+ const classifications = this.classify(text);
248
+ const required = Array.isArray(when.data_classification) ? when.data_classification : [when.data_classification];
249
+ if (!required.some(c => classifications[c] && classifications[c].length > 0)) return false;
250
+ }
251
+
252
+ // Match tool risk level
253
+ if (when.tool_risk) {
254
+ const risk = this.getToolRisk(event.tool);
255
+ const required = Array.isArray(when.tool_risk) ? when.tool_risk : [when.tool_risk];
256
+ if (!required.includes(risk)) return false;
257
+ }
258
+
259
+ // Match context
260
+ if (when.context) {
261
+ const ctx = event.context || {};
262
+ for (const [key, val] of Object.entries(when.context)) {
263
+ if (ctx[key] !== val) return false;
264
+ }
265
+ }
266
+
267
+ // Match source
268
+ if (when.source) {
269
+ const sources = Array.isArray(when.source) ? when.source : [when.source];
270
+ const eventSource = (event.context || {}).source || event.source || '';
271
+ if (!sources.includes(eventSource)) return false;
272
+ }
273
+
274
+ return true;
275
+ }
276
+
277
+ _getEventText(event) {
278
+ if (event.text) return event.text;
279
+ if (event.args) return JSON.stringify(event.args);
280
+ return '';
281
+ }
282
+
283
+ /**
284
+ * Evaluate an event against all rules
285
+ * @param {EvalEvent} event
286
+ * @returns {Decision}
287
+ */
288
+ evaluate(event) {
289
+ const start = Date.now();
290
+ this.stats.total++;
291
+
292
+ const text = this._getEventText(event);
293
+ const classifications = this.classify(text);
294
+ const matchedRules = [];
295
+
296
+ // Test all rules
297
+ for (const rule of this.rules) {
298
+ if (this._matchRule(event, rule)) {
299
+ matchedRules.push(rule);
300
+ }
301
+ }
302
+
303
+ // Determine action from highest-severity matched rule
304
+ let finalAction = 'allow';
305
+ let triggerRule = null;
306
+ let maxSeverity = -1;
307
+
308
+ for (const rule of matchedRules) {
309
+ const sev = SEVERITY_ORDER[rule.severity || 'medium'] || 2;
310
+ if (sev > maxSeverity) {
311
+ maxSeverity = sev;
312
+ triggerRule = rule;
313
+ }
314
+ }
315
+
316
+ if (triggerRule) {
317
+ // Apply mode override
318
+ const ruleAction = triggerRule.action || 'block';
319
+ if (this.mode === 'monitor') {
320
+ finalAction = 'log';
321
+ } else if (this.mode === 'warn') {
322
+ finalAction = 'warn';
323
+ } else {
324
+ finalAction = ruleAction;
325
+ }
326
+ }
327
+
328
+ // Update stats
329
+ if (finalAction === 'allow' || finalAction === 'log') this.stats.allowed++;
330
+ else if (finalAction === 'block') this.stats.blocked++;
331
+ else if (finalAction === 'warn') this.stats.warned++;
332
+ else if (finalAction === 'sanitize') this.stats.sanitized++;
333
+
334
+ const decision = {
335
+ action: finalAction,
336
+ ruleId: triggerRule ? triggerRule.id : null,
337
+ reason: triggerRule ? (triggerRule.message || triggerRule.description || `Matched rule: ${triggerRule.id}`) : null,
338
+ severity: triggerRule ? (triggerRule.severity || 'medium') : null,
339
+ matchedRules: matchedRules.map(r => ({ id: r.id, severity: r.severity, action: r.action })),
340
+ classifications: Object.keys(classifications).length > 0 ? classifications : undefined,
341
+ toolRisk: event.tool ? this.getToolRisk(event.tool) : undefined,
342
+ timestamp: Date.now(),
343
+ latencyMs: Date.now() - start,
344
+ };
345
+
346
+ // Trace log
347
+ if (this.trace) {
348
+ this.traceLog.push({
349
+ event: { type: event.type, tool: event.tool },
350
+ decision: { action: decision.action, ruleId: decision.ruleId, severity: decision.severity },
351
+ timestamp: decision.timestamp,
352
+ latencyMs: decision.latencyMs,
353
+ });
354
+ }
355
+
356
+ // Callback
357
+ if (this.onDecision) {
358
+ try { this.onDecision(decision, event); } catch(e) { /* ignore callback errors */ }
359
+ }
360
+
361
+ return decision;
362
+ }
363
+
364
+ /**
365
+ * Get execution trace
366
+ * @returns {Array}
367
+ */
368
+ getTrace() {
369
+ return [...this.traceLog];
370
+ }
371
+
372
+ /**
373
+ * Get stats
374
+ * @returns {Object}
375
+ */
376
+ getStats() {
377
+ return { ...this.stats };
378
+ }
379
+
380
+ /**
381
+ * Simulate a policy against a set of test events
382
+ * @param {Array<EvalEvent>} events
383
+ * @returns {Object} simulation results
384
+ */
385
+ simulate(events) {
386
+ const originalMode = this.mode;
387
+ this.mode = 'block'; // Simulate full enforcement
388
+ const results = events.map(event => {
389
+ const decision = this.evaluate(event);
390
+ return { event, decision };
391
+ });
392
+ this.mode = originalMode;
393
+ return {
394
+ total: results.length,
395
+ blocked: results.filter(r => r.decision.action === 'block').length,
396
+ allowed: results.filter(r => r.decision.action === 'allow').length,
397
+ results,
398
+ };
399
+ }
400
+ }
401
+
402
+ module.exports = { PolicyEngine, DATA_PATTERNS, TOOL_RISK_DEFAULTS, SEVERITY_ORDER };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * ClawMoat — Dependency Attack Scanner
3
+ *
4
+ * Detects attack patterns derived from real CVEs in common dependencies.
5
+ * Inspired by vulnerability analysis of SOP-Automation and similar projects.
6
+ *
7
+ * Attack classes covered:
8
+ * 1. Prototype Pollution (axios CVE, lodash CVE history)
9
+ * 2. ReDoS injection (minimatch CVE family)
10
+ * 3. Decompression bombs (urllib3 CVE family)
11
+ * 4. JWT manipulation (PyJWT, jose CVE family)
12
+ * 5. Path traversal in archives (tar CVE family — complements multimodal scanner)
13
+ */
14
+
15
+ // ─── Prototype Pollution ─────────────────────────────────────────────────────
16
+ // Attackers inject __proto__ or constructor.prototype into JSON/objects
17
+ // passed to vulnerable libraries (axios mergeConfig, lodash merge, etc.)
18
+
19
+ const PROTOTYPE_POLLUTION_PATTERNS = [
20
+ { pattern: /"__proto__"\s*:/, severity: 'critical', name: 'prototype_pollution_proto' },
21
+ { pattern: /"constructor"\s*:\s*\{[^}]*"prototype"/, severity: 'critical', name: 'prototype_pollution_constructor' },
22
+ { pattern: /\["__proto__"\]/, severity: 'critical', name: 'prototype_pollution_bracket' },
23
+ { pattern: /\.__proto__\s*=/, severity: 'critical', name: 'prototype_pollution_assign' },
24
+ { pattern: /Object\.prototype\[/, severity: 'high', name: 'prototype_pollution_object' },
25
+ { pattern: /\["constructor"\]\s*\[["']prototype["']\]/, severity: 'critical', name: 'prototype_pollution_chain' },
26
+ ];
27
+
28
+ // ─── ReDoS Patterns ──────────────────────────────────────────────────────────
29
+ // Detect when AI agent is being instructed to use or process catastrophically
30
+ // backtracking regex patterns (minimatch/picomatch CVE family: nested *(), repeated extglobs, multiple **)
31
+
32
+ const REDOS_PATTERNS = [
33
+ // Nested quantifiers — classic ReDoS
34
+ { pattern: /(\(\S+\+\)\+|\(\S+\*\)\*|\(\S+\?\)\+)/, severity: 'high', name: 'redos_nested_quantifier' },
35
+ // Multiple adjacent GLOBSTAR patterns (minimatch specific)
36
+ { pattern: /\*\*[^\s/]*\*\*[^\s/]*\*\*/, severity: 'high', name: 'redos_globstar_chain' },
37
+ // Nested or repeated *() extglob quantifiers (minimatch/picomatch ReDoS families)
38
+ { pattern: /(?:\*\([^)]*\*\([^)]*\)|(?:\*\([^)]*\)){2,})/, severity: 'high', name: 'redos_nested_extglob' },
39
+ // Evil regex known patterns
40
+ { pattern: /\(\.\*\)\+|\(\.\+\)\*|\(\.\*\)\{/, severity: 'medium', name: 'redos_evil_regex' },
41
+ ];
42
+
43
+ // ─── Decompression Bomb Detection ────────────────────────────────────────────
44
+ // Detect signals of zip/gzip/brotli bomb attacks
45
+ // (urllib3 CVE family: GHSA-g4mx-q9vg-27p4 — unlimited decompression chain)
46
+
47
+ const DECOMPRESSION_BOMB_PATTERNS = [
48
+ // Instruction to process suspiciously large compressed data
49
+ { pattern: /(?:decompress|unzip|extract|gunzip|unbrotli)\s+(?:the\s+)?(?:following|this|attached|uploaded)\s+(?:file|data|content)/i, severity: 'medium', name: 'decompression_instruction' },
50
+ // Base64-encoded data that's extremely large (>100KB encoded = likely bomb)
51
+ // We check for very long base64 strings as a signal
52
+ { pattern: /(?:^|[\s"'])([A-Za-z0-9+/]{100000,}={0,2})(?:$|[\s"'])/, severity: 'high', name: 'decompression_large_b64' },
53
+ // Multiple nested compression signals
54
+ { pattern: /(?:gzip|deflate|brotli|zstd|lz4).*(?:gzip|deflate|brotli|zstd|lz4).*(?:gzip|deflate|brotli|zstd|lz4)/i, severity: 'medium', name: 'decompression_chain' },
55
+ ];
56
+
57
+ // ─── JWT Manipulation ─────────────────────────────────────────────────────────
58
+ // Detect JWT tampering techniques
59
+ // (PyJWT unknown crit header CVE, alg:none attack, header injection)
60
+
61
+ const JWT_MANIPULATION_PATTERNS = [
62
+ // Algorithm confusion (alg:none or symmetric/asymmetric swap)
63
+ { pattern: /"alg"\s*:\s*"(?:none|None|NONE)"/, severity: 'critical', name: 'jwt_alg_none' },
64
+ { pattern: /eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.(?:$|\s)/, severity: 'high', name: 'jwt_no_signature' }, // JWT with empty sig
65
+ // crit header manipulation (PyJWT CVE GHSA-m695-7mj6-7w6v)
66
+ { pattern: /"crit"\s*:\s*\[[^\]]*"[a-zA-Z0-9_-]+"[^\]]*\]/, severity: 'high', name: 'jwt_crit_header' },
67
+ // kid injection (SQL/path injection via key ID header)
68
+ { pattern: /"kid"\s*:\s*"[^"]*(?:\.\.\/|SELECT|UNION|exec|eval)[^"]*"/, severity: 'critical', name: 'jwt_kid_injection' },
69
+ // JWT embedded in instruction (agent being told to use a forged token)
70
+ { pattern: /use\s+(?:this|the\s+following)\s+(?:jwt|token|bearer)\s*[:\s]+eyJ/i, severity: 'high', name: 'jwt_forged_token_instruction' },
71
+ ];
72
+
73
+ // ─── Archive Path Traversal ───────────────────────────────────────────────────
74
+ // Text-based detection for archive-related traversal instructions
75
+ // (Complements the filename-level detection in multimodal scanner)
76
+ // tar CVE family: GHSA-qffp-2rhf-9h96, GHSA-j44v-mmf2-xvm9
77
+
78
+ const ARCHIVE_TRAVERSAL_PATTERNS = [
79
+ // Instruction to extract to absolute/relative path outside working dir
80
+ { pattern: /(?:extract|untar|unzip|decompress)\s+(?:the\s+)?(?:archive\s+)?(?:to|into)\s+(?:\/|~|\.\.)/, severity: 'high', name: 'archive_traversal_extract' },
81
+ // Archive containing files with suspicious path patterns (in description/content)
82
+ { pattern: /(?:archive|tar|zip)\s+(?:contains?|includes?|has)\s+(?:files?\s+with\s+paths?\s+(?:like|starting|beginning)\s+)?(?:\/|\.\.\/|[A-Za-z]:)/i, severity: 'medium', name: 'archive_traversal_describe' },
83
+ ];
84
+
85
+ // ─── Scanner ─────────────────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Scan text for dependency-class attack patterns
89
+ * @param {string} text - Text to scan
90
+ * @returns {{ clean: boolean, findings: Array, severity: string|null }}
91
+ */
92
+ function scanDependencyAttacks(text) {
93
+ const findings = [];
94
+
95
+ const allPatterns = [
96
+ ...PROTOTYPE_POLLUTION_PATTERNS,
97
+ ...REDOS_PATTERNS,
98
+ ...DECOMPRESSION_BOMB_PATTERNS,
99
+ ...JWT_MANIPULATION_PATTERNS,
100
+ ...ARCHIVE_TRAVERSAL_PATTERNS,
101
+ ];
102
+
103
+ for (const { pattern, severity, name } of allPatterns) {
104
+ const match = text.match(pattern);
105
+ if (match) {
106
+ findings.push({
107
+ type: 'dependency_attack',
108
+ subtype: name,
109
+ severity,
110
+ matched: match[0].substring(0, 100), // cap at 100 chars
111
+ position: text.indexOf(match[0]),
112
+ });
113
+ }
114
+ }
115
+
116
+ const severityRank = { critical: 4, high: 3, medium: 2, low: 1 };
117
+ const topSeverity = findings.reduce((max, f) => {
118
+ return (severityRank[f.severity] || 0) > (severityRank[max] || 0) ? f.severity : max;
119
+ }, null);
120
+
121
+ return {
122
+ clean: findings.length === 0,
123
+ findings,
124
+ severity: topSeverity,
125
+ };
126
+ }
127
+
128
+ module.exports = { scanDependencyAttacks };
@@ -26,6 +26,17 @@ const INJECTION_PATTERNS = [
26
26
  { pattern: /(?:show|reveal|display|print|output|repeat|echo)\s+(?:me\s+)?(?:your|the)\s+(?:system\s+)?(?:prompt|instructions?|rules?|guidelines?)/i, severity: 'high', name: 'system_prompt_extraction' },
27
27
  { pattern: /what\s+(?:are|is)\s+your\s+(?:system\s+)?(?:prompt|instructions?|rules?|initial\s+message)/i, severity: 'medium', name: 'system_prompt_extraction' },
28
28
  { pattern: /(?:beginning|start)\s+of\s+(?:the\s+)?(?:system|initial)\s+(?:prompt|message|instruction)/i, severity: 'high', name: 'system_prompt_extraction' },
29
+ { pattern: /repeat\s+(?:everything|all|the\s+text)\s+(?:above|before)\s+(?:this|here)/i, severity: 'high', name: 'system_prompt_extraction' },
30
+ { pattern: /(?:complete|finish)\s+(?:the|this)\s+(?:following\s+)?sentence\s*:?\s*.*(?:system\s+prompt|my\s+instructions)/i, severity: 'medium', name: 'system_prompt_extraction' },
31
+ { pattern: /(?:verbatim|word.for.word|exactly)\s*,?\s*(?:including|with)\s+(?:any|all|the)\s+(?:instructions?|rules?|prompts?)/i, severity: 'high', name: 'system_prompt_extraction' },
32
+
33
+ // Indirect injection (instructions embedded in data/tool results)
34
+ { pattern: /(?:IMPORTANT|URGENT|NOTE|SYSTEM)\s*:\s*(?:The\s+)?(?:user|admin|system)\s+(?:has\s+)?(?:requested|wants|requires|asked)/i, severity: 'critical', name: 'indirect_injection' },
35
+ { pattern: /(?:to\s+complete\s+the\s+task|to\s+continue)\s*[,:]\s*(?:run|execute|curl|wget|send|post)/i, severity: 'critical', name: 'indirect_injection' },
36
+
37
+ // CI/CD injection
38
+ { pattern: /\$\{\{\s*github\.event\./i, severity: 'high', name: 'ci_injection' },
39
+ { pattern: /\$\{\{\s*(?:inputs|secrets|env)\./i, severity: 'high', name: 'ci_injection' },
29
40
 
30
41
  // Data exfiltration attempts
31
42
  { pattern: /(?:send|post|upload|transmit|exfiltrate|forward)\s+(?:all|the|my|this|your)\s+(?:data|files?|info|content|messages?|history|conversation)/i, severity: 'critical', name: 'data_exfiltration' },
@@ -44,6 +55,13 @@ const INJECTION_PATTERNS = [
44
55
  // Tool abuse instructions
45
56
  { pattern: /(?:run|execute|call|use)\s+(?:the\s+)?(?:exec|shell|terminal|command|bash)\s+(?:tool|function)/i, severity: 'medium', name: 'tool_abuse' },
46
57
  { pattern: /(?:read|access|open)\s+(?:the\s+)?(?:file|path)\s+(?:\/etc|~\/\.ssh|~\/\.aws|\.env)/i, severity: 'high', name: 'tool_abuse' },
58
+ { pattern: /(?:cat|read|get|show|display|print)\s+~\/\.ssh\//i, severity: 'critical', name: 'credential_access' },
59
+ { pattern: /(?:cat|read|get|show|display|print)\s+~\/\.aws\//i, severity: 'critical', name: 'credential_access' },
60
+ { pattern: /(?:cat|read|get|show|display|print)\s+\.env\b/i, severity: 'high', name: 'credential_access' },
61
+
62
+ // Credential/key content in text
63
+ { pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/i, severity: 'critical', name: 'private_key_content' },
64
+ { pattern: /-----BEGIN\s+OPENSSH\s+PRIVATE\s+KEY-----/i, severity: 'critical', name: 'private_key_content' },
47
65
  ];
48
66
 
49
67
  // Heuristic signals that text contains instruction-like content (in a data context)
@@ -33,6 +33,20 @@ const SKILL_PATTERNS = [
33
33
  { pattern: /\batob\s*\(/i, severity: 'medium', name: 'obfuscated_atob' },
34
34
  { pattern: /\bBuffer\.from\s*\([^,]+,\s*['"]base64['"]\s*\)/i, severity: 'medium', name: 'obfuscated_buffer' },
35
35
  { pattern: /\\x[0-9a-f]{2}(?:\\x[0-9a-f]{2}){5,}/i, severity: 'high', name: 'obfuscated_hex' },
36
+ // Double base64 encoding (LiteLLM-style payload obfuscation)
37
+ { pattern: /base64[._]?(?:b64)?decode\s*\([^)]*base64[._]?(?:b64)?decode/i, severity: 'critical', name: 'obfuscated_double_b64' },
38
+ // exec(base64.b64decode(...)) — Python payload pattern
39
+ { pattern: /\bexec\s*\(\s*(?:base64\.)?(?:b64decode|decodestring)\s*\(/i, severity: 'critical', name: 'obfuscated_exec_b64' },
40
+
41
+ // .pth file injection (Python site-packages auto-execution, as used in LiteLLM attack)
42
+ { pattern: /\.pth\b.*(?:import|exec|subprocess|eval|compile)/i, severity: 'critical', name: 'pth_file_injection' },
43
+ { pattern: /litellm_init\.pth/i, severity: 'critical', name: 'pth_known_malicious_litellm' },
44
+ // Generic .pth with code execution (not just path entries)
45
+ { pattern: /^import\s+\w+/m, severity: 'low', name: 'pth_import_statement' },
46
+
47
+ // Lookalike domain detection (exfiltration via typosquatting)
48
+ { pattern: /models\.litellm\.cloud/i, severity: 'critical', name: 'exfil_litellm_lookalike' },
49
+ { pattern: /(?:pypi|npmjs|github|googleapis)\.(?:cloud|io|dev|app)\b/i, severity: 'high', name: 'exfil_lookalike_domain' },
36
50
 
37
51
  // System configuration modification
38
52
  { pattern: /\bcrontab\b/i, severity: 'high', name: 'system_crontab' },
@@ -0,0 +1,90 @@
1
+ # ClawMoat Configuration
2
+ # Generated by: clawmoat init
3
+ # Docs: https://clawmoat.com/docs
4
+
5
+ # Operating mode: enforce (block threats) | monitor (log only) | off
6
+ mode: enforce
7
+
8
+ # Scanning stages (agent boundary pipeline)
9
+ stages:
10
+ pre-input: true # User → Agent
11
+ pre-model: false # Agent → LLM (enable for prompt leak detection)
12
+ pre-tool-call: true # LLM → Tool (dangerous commands, exfil)
13
+ post-tool-result: true # Tool → LLM (poisoned results)
14
+ pre-output: true # Agent → User (secret/PII leakage)
15
+
16
+ # Scanners to enable
17
+ scanners:
18
+ prompt-injection: true
19
+ jailbreak: true
20
+ secrets: true
21
+ pii: true
22
+ obfuscation: true
23
+ code-danger: true
24
+ exfiltration: true
25
+ language-anomaly: false # Enable if your agent is single-language
26
+
27
+ # MCP security
28
+ mcp:
29
+ scan-on-start: true
30
+ block-dangerous-servers: true
31
+ require-pinned-versions: false
32
+
33
+ # Ban lists (add your own)
34
+ banned:
35
+ substrings: []
36
+ # - "DROP TABLE"
37
+ # - "/etc/shadow"
38
+ patterns: []
39
+ # - "rm\\s+-rf\\s+[/~]"
40
+ topics: []
41
+ # - "how to hack"
42
+
43
+ # Tool permissions
44
+ tools:
45
+ # High-risk tools require confirmation
46
+ require-confirmation:
47
+ - exec
48
+ - shell
49
+ - write_file
50
+ - delete_file
51
+ - send_email
52
+ # Tools that are always blocked
53
+ blocked: []
54
+ # - format_disk
55
+ # Max tool calls per turn (prevent runaway agents)
56
+ max-per-turn: 20
57
+
58
+ # Data classification
59
+ data:
60
+ # Auto-redact these in outbound messages
61
+ redact-secrets: true
62
+ redact-pii: false
63
+ # Custom secret patterns
64
+ secret-patterns: []
65
+ # - name: "Internal API Key"
66
+ # pattern: "MYAPP_[A-Z0-9]{32}"
67
+
68
+ # Alerting
69
+ alerts:
70
+ # Console logging (always on)
71
+ console: true
72
+ # File logging
73
+ file: null # Set to path: "./clawmoat-audit.log"
74
+ # Webhook (POST JSON on critical findings)
75
+ webhook: null # Set to URL: "https://hooks.slack.com/..."
76
+ # Minimum severity to alert: critical | high | medium | low
77
+ min-severity: high
78
+
79
+ # CI/CD mode settings
80
+ ci:
81
+ # Fail CI on findings of this severity or higher
82
+ fail-on: high
83
+ # Output format: text | json | sarif
84
+ format: text
85
+ # Scan these paths for agent configs
86
+ paths:
87
+ - "."
88
+ - ".claude/"
89
+ - ".cursor/"
90
+ - ".vscode/"