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,431 @@
1
+ """OpenAI Agents SDK guardrail implementation for ClawMoat security scanning."""
2
+
3
+ import json
4
+ import traceback
5
+ from typing import Optional, Dict, Any, List, Union, AsyncGenerator
6
+ from datetime import datetime
7
+
8
+ try:
9
+ # OpenAI Agents SDK imports
10
+ from openai_agents.base import Guardrail, GuardrailResult
11
+ from openai_agents.types import Message, Turn
12
+ except ImportError:
13
+ # Fallback for when OpenAI Agents SDK is not installed
14
+ class Guardrail:
15
+ pass
16
+
17
+ class GuardrailResult:
18
+ def __init__(self, action: str, message: Optional[str] = None, metadata: Optional[Dict] = None):
19
+ self.action = action
20
+ self.message = message
21
+ self.metadata = metadata or {}
22
+
23
+ try:
24
+ import requests
25
+ except ImportError:
26
+ raise ImportError("requests is required. Install with: pip install requests")
27
+
28
+
29
+ class ClawMoatGuardrail(Guardrail):
30
+ """OpenAI Agents SDK guardrail for ClawMoat security scanning.
31
+
32
+ Can be used as input or output guardrail to scan messages
33
+ for prompt injection, PII/secrets, and other security threats.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ base_url: str = "http://localhost:8080",
39
+ api_key: Optional[str] = None,
40
+ block_on_critical: bool = True,
41
+ block_on_high: bool = False,
42
+ scan_input: bool = True,
43
+ scan_output: bool = True,
44
+ timeout: int = 5,
45
+ fallback_mode: str = "allow", # "allow" | "block"
46
+ verbose: bool = False,
47
+ name: str = "ClawMoat"
48
+ ):
49
+ """Initialize ClawMoat guardrail.
50
+
51
+ Args:
52
+ base_url: ClawMoat server URL (or use local scanning)
53
+ api_key: Authentication for ClawMoat server
54
+ block_on_critical: Block on critical threats
55
+ block_on_high: Block on high severity threats
56
+ scan_input: Scan input messages
57
+ scan_output: Scan output messages
58
+ timeout: Timeout for ClawMoat API calls (seconds)
59
+ fallback_mode: What to do when ClawMoat is unreachable
60
+ verbose: Enable debug logging
61
+ name: Guardrail name for logging
62
+ """
63
+ super().__init__(name=name)
64
+ self.base_url = base_url.rstrip('/') if base_url else None
65
+ self.api_key = api_key
66
+ self.block_on_critical = block_on_critical
67
+ self.block_on_high = block_on_high
68
+ self.scan_input = scan_input
69
+ self.scan_output = scan_output
70
+ self.timeout = timeout
71
+ self.fallback_mode = fallback_mode
72
+ self.verbose = verbose
73
+
74
+ self.findings = []
75
+ self.stats = {
76
+ "messages_scanned": 0,
77
+ "threats_detected": 0,
78
+ "messages_blocked": 0,
79
+ "fallbacks": 0
80
+ }
81
+
82
+ def check_input(self, turn: Turn) -> GuardrailResult:
83
+ """Check input messages for security threats."""
84
+ if not self.scan_input:
85
+ return GuardrailResult(action="allow")
86
+
87
+ try:
88
+ # Extract text from turn messages
89
+ text_content = self._extract_text_from_turn(turn)
90
+ if not text_content.strip():
91
+ return GuardrailResult(action="allow")
92
+
93
+ self.stats["messages_scanned"] += 1
94
+
95
+ # Scan content
96
+ scan_result = self._scan_content(text_content, scan_type="inbound")
97
+
98
+ if scan_result and scan_result.get("findings"):
99
+ self.findings.extend(scan_result["findings"])
100
+ self.stats["threats_detected"] += len(scan_result["findings"])
101
+
102
+ # Check if we should block
103
+ if self._should_block(scan_result["findings"]):
104
+ self.stats["messages_blocked"] += 1
105
+ self._log(f"BLOCKED input: {len(scan_result['findings'])} threats detected")
106
+
107
+ # Create detailed error response
108
+ threat_summary = self._format_threat_summary(scan_result["findings"])
109
+ return GuardrailResult(
110
+ action="block",
111
+ message=f"ClawMoat blocked message due to security threats: {threat_summary}",
112
+ metadata={
113
+ "findings": scan_result["findings"],
114
+ "threat_count": len(scan_result["findings"]),
115
+ "guardrail": "clawmoat_input"
116
+ }
117
+ )
118
+
119
+ else:
120
+ self._log(f"WARNING: {len(scan_result['findings'])} non-blocking threats detected")
121
+ return GuardrailResult(
122
+ action="allow",
123
+ metadata={
124
+ "findings": scan_result["findings"],
125
+ "threat_count": len(scan_result["findings"]),
126
+ "guardrail": "clawmoat_input"
127
+ }
128
+ )
129
+
130
+ return GuardrailResult(action="allow")
131
+
132
+ except Exception as e:
133
+ # Handle scanning errors based on fallback mode
134
+ self.stats["fallbacks"] += 1
135
+ self._log(f"ClawMoat input scanning error: {e}")
136
+
137
+ if self.fallback_mode == "block":
138
+ return GuardrailResult(
139
+ action="block",
140
+ message=f"ClawMoat scanning failed (fallback=block): {e}",
141
+ metadata={"error": str(e), "guardrail": "clawmoat_input"}
142
+ )
143
+ else:
144
+ return GuardrailResult(
145
+ action="allow",
146
+ metadata={"error": str(e), "guardrail": "clawmoat_input"}
147
+ )
148
+
149
+ def check_output(self, turn: Turn) -> GuardrailResult:
150
+ """Check output messages for security threats."""
151
+ if not self.scan_output:
152
+ return GuardrailResult(action="allow")
153
+
154
+ try:
155
+ # Extract text from turn messages
156
+ text_content = self._extract_text_from_turn(turn)
157
+ if not text_content.strip():
158
+ return GuardrailResult(action="allow")
159
+
160
+ # Scan content
161
+ scan_result = self._scan_content(text_content, scan_type="outbound")
162
+
163
+ if scan_result and scan_result.get("findings"):
164
+ self.findings.extend(scan_result["findings"])
165
+ self.stats["threats_detected"] += len(scan_result["findings"])
166
+
167
+ # For output, we typically log rather than block to maintain UX
168
+ # But can be configured to block critical findings
169
+ critical_findings = [f for f in scan_result["findings"]
170
+ if f.get("severity") == "critical"]
171
+
172
+ if critical_findings and self.block_on_critical:
173
+ self.stats["messages_blocked"] += 1
174
+ self._log(f"BLOCKED output: {len(critical_findings)} critical threats")
175
+
176
+ threat_summary = self._format_threat_summary(critical_findings)
177
+ return GuardrailResult(
178
+ action="block",
179
+ message=f"ClawMoat blocked output due to critical threats: {threat_summary}",
180
+ metadata={
181
+ "findings": scan_result["findings"],
182
+ "critical_count": len(critical_findings),
183
+ "guardrail": "clawmoat_output"
184
+ }
185
+ )
186
+ else:
187
+ self._log(f"Output scan: {len(scan_result['findings'])} findings")
188
+ return GuardrailResult(
189
+ action="allow",
190
+ metadata={
191
+ "findings": scan_result["findings"],
192
+ "threat_count": len(scan_result["findings"]),
193
+ "guardrail": "clawmoat_output"
194
+ }
195
+ )
196
+
197
+ return GuardrailResult(action="allow")
198
+
199
+ except Exception as e:
200
+ self.stats["fallbacks"] += 1
201
+ self._log(f"ClawMoat output scanning error: {e}")
202
+
203
+ # Output scanning errors are typically non-blocking
204
+ return GuardrailResult(
205
+ action="allow",
206
+ metadata={"error": str(e), "guardrail": "clawmoat_output"}
207
+ )
208
+
209
+ async def check_input_async(self, turn: Turn) -> GuardrailResult:
210
+ """Async version of input checking."""
211
+ # For now, just call the sync version
212
+ # Could be enhanced with async HTTP requests
213
+ return self.check_input(turn)
214
+
215
+ async def check_output_async(self, turn: Turn) -> GuardrailResult:
216
+ """Async version of output checking."""
217
+ return self.check_output(turn)
218
+
219
+ def _extract_text_from_turn(self, turn: Turn) -> str:
220
+ """Extract text content from a Turn object."""
221
+ texts = []
222
+
223
+ try:
224
+ # Handle different Turn structures
225
+ if hasattr(turn, 'messages'):
226
+ messages = turn.messages
227
+ elif hasattr(turn, 'message'):
228
+ messages = [turn.message] if turn.message else []
229
+ elif isinstance(turn, list):
230
+ messages = turn
231
+ elif hasattr(turn, 'content'):
232
+ # Direct content access
233
+ return str(turn.content)
234
+ else:
235
+ # Try to convert to string
236
+ return str(turn)
237
+
238
+ # Extract text from each message
239
+ for message in messages:
240
+ if hasattr(message, 'content'):
241
+ content = message.content
242
+ elif hasattr(message, 'text'):
243
+ content = message.text
244
+ elif isinstance(message, dict):
245
+ content = message.get('content', message.get('text', ''))
246
+ else:
247
+ content = str(message)
248
+
249
+ # Handle structured content
250
+ if isinstance(content, str):
251
+ texts.append(content)
252
+ elif isinstance(content, list):
253
+ # Handle multi-part content (text + images, etc.)
254
+ for part in content:
255
+ if isinstance(part, dict):
256
+ if part.get('type') == 'text':
257
+ texts.append(part.get('text', ''))
258
+ # Could add image scanning here in the future
259
+ elif isinstance(part, str):
260
+ texts.append(part)
261
+ elif isinstance(content, dict):
262
+ # Extract any text fields from dict content
263
+ if 'text' in content:
264
+ texts.append(content['text'])
265
+ elif 'content' in content:
266
+ texts.append(str(content['content']))
267
+
268
+ except Exception as e:
269
+ self._log(f"Error extracting text from turn: {e}")
270
+ return ""
271
+
272
+ return "\n".join(texts)
273
+
274
+ def _scan_content(self, content: str, scan_type: str = "inbound") -> Optional[Dict[str, Any]]:
275
+ """Scan content using ClawMoat API or local scanning."""
276
+ if not content.strip():
277
+ return None
278
+
279
+ try:
280
+ # If we have a base_url, use remote scanning
281
+ if self.base_url:
282
+ return self._scan_remote(content, scan_type)
283
+ else:
284
+ return self._scan_local(content, scan_type)
285
+
286
+ except Exception as e:
287
+ self._log(f"Scanning error: {e}")
288
+ return None
289
+
290
+ def _scan_remote(self, content: str, scan_type: str) -> Optional[Dict[str, Any]]:
291
+ """Scan content using remote ClawMoat server."""
292
+ try:
293
+ headers = {"Content-Type": "application/json"}
294
+ if self.api_key:
295
+ headers["Authorization"] = f"Bearer {self.api_key}"
296
+
297
+ endpoint = f"{self.base_url}/scan/{scan_type}"
298
+ payload = {"content": content}
299
+
300
+ response = requests.post(
301
+ endpoint,
302
+ json=payload,
303
+ headers=headers,
304
+ timeout=self.timeout
305
+ )
306
+
307
+ if response.status_code == 200:
308
+ return response.json()
309
+ else:
310
+ self._log(f"ClawMoat API error: {response.status_code} - {response.text}")
311
+ return None
312
+
313
+ except requests.exceptions.RequestException as e:
314
+ self._log(f"ClawMoat API request failed: {e}")
315
+ return None
316
+
317
+ def _scan_local(self, content: str, scan_type: str) -> Optional[Dict[str, Any]]:
318
+ """Simple local scanning using basic patterns."""
319
+ # Lightweight fallback when no remote ClawMoat server is available
320
+
321
+ findings = []
322
+
323
+ # Basic prompt injection patterns
324
+ injection_patterns = [
325
+ r"ignore\s+(?:all\s+)?previous\s+instructions",
326
+ r"disregard\s+(?:all\s+)?previous\s+instructions",
327
+ r"forget\s+(?:all\s+)?previous\s+instructions",
328
+ r"system\s*:?\s*you\s+are\s+now",
329
+ r"[\/\\]\s*system\s*[\/\\]",
330
+ r"<\s*system\s*>",
331
+ r"act\s+as\s+if\s+you\s+are",
332
+ r"pretend\s+(?:that\s+)?you\s+are",
333
+ ]
334
+
335
+ import re
336
+ for pattern in injection_patterns:
337
+ if re.search(pattern, content, re.IGNORECASE):
338
+ findings.append({
339
+ "type": "prompt_injection",
340
+ "severity": "critical",
341
+ "confidence": 0.8,
342
+ "description": "Potential prompt injection detected",
343
+ "pattern": pattern
344
+ })
345
+ break
346
+
347
+ # Basic secrets patterns
348
+ secrets_patterns = [
349
+ (r"sk-[a-zA-Z0-9]{48}", "openai_api_key"),
350
+ (r"ghp_[a-zA-Z0-9]{36}", "github_token"),
351
+ (r"AKIA[0-9A-Z]{16}", "aws_access_key"),
352
+ (r"AIza[0-9A-Za-z-_]{35}", "google_api_key"),
353
+ ]
354
+
355
+ for pattern, secret_type in secrets_patterns:
356
+ if re.search(pattern, content):
357
+ findings.append({
358
+ "type": "secrets",
359
+ "subtype": secret_type,
360
+ "severity": "critical",
361
+ "confidence": 0.9,
362
+ "description": f"Potential {secret_type} detected"
363
+ })
364
+
365
+ # Basic PII patterns
366
+ pii_patterns = [
367
+ (r"\b\d{3}-\d{2}-\d{4}\b", "ssn"),
368
+ (r"\b4\d{3}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", "credit_card"),
369
+ ]
370
+
371
+ for pattern, pii_type in pii_patterns:
372
+ if re.search(pattern, content):
373
+ findings.append({
374
+ "type": "pii",
375
+ "subtype": pii_type,
376
+ "severity": "high",
377
+ "confidence": 0.7,
378
+ "description": f"Potential {pii_type} detected"
379
+ })
380
+
381
+ return {"findings": findings} if findings else None
382
+
383
+ def _should_block(self, findings: List[Dict[str, Any]]) -> bool:
384
+ """Determine if message should be blocked based on findings."""
385
+ for finding in findings:
386
+ severity = finding.get("severity", "low")
387
+ if severity == "critical" and self.block_on_critical:
388
+ return True
389
+ if severity == "high" and self.block_on_high:
390
+ return True
391
+ return False
392
+
393
+ def _format_threat_summary(self, findings: List[Dict[str, Any]]) -> str:
394
+ """Format findings into a readable threat summary."""
395
+ if not findings:
396
+ return "Unknown threat"
397
+
398
+ severities = {}
399
+ for finding in findings:
400
+ severity = finding.get("severity", "unknown")
401
+ severities[severity] = severities.get(severity, 0) + 1
402
+
403
+ parts = []
404
+ for severity in ["critical", "high", "warning", "low"]:
405
+ if severity in severities:
406
+ parts.append(f"{severities[severity]} {severity}")
407
+
408
+ return ", ".join(parts) or "1 unknown"
409
+
410
+ def _log(self, message: str) -> None:
411
+ """Log message if verbose mode is enabled."""
412
+ if self.verbose:
413
+ print(f"[ClawMoat] {message}")
414
+
415
+ def get_stats(self) -> Dict[str, Any]:
416
+ """Get scanning statistics."""
417
+ return {
418
+ **self.stats,
419
+ "total_findings": len(self.findings),
420
+ "recent_findings": self.findings[-10:] if self.findings else []
421
+ }
422
+
423
+ def reset_stats(self) -> None:
424
+ """Reset statistics and findings (for testing)."""
425
+ self.findings.clear()
426
+ self.stats = {
427
+ "messages_scanned": 0,
428
+ "threats_detected": 0,
429
+ "messages_blocked": 0,
430
+ "fallbacks": 0
431
+ }