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.
- package/.dockerignore +9 -0
- package/CHANGELOG.md +18 -0
- package/CONTRIBUTING.md +4 -2
- package/DEMO.md +87 -0
- package/Dockerfile +5 -18
- package/README.md +294 -8
- package/SECURITY.md +58 -10
- package/THREAT_MODEL.md +129 -0
- package/agent/README.md +131 -0
- package/agent/index.js +471 -0
- package/agent/install-service.sh +94 -0
- package/agent/openclaw-hook.js +453 -0
- package/agent/provider-setup.js +649 -0
- package/agent/setup.js +274 -0
- package/assets/BADGE-USAGE.md +20 -0
- package/assets/clawmoat-badge.svg +21 -0
- package/bin/clawmoat.js +468 -111
- package/docs/affiliates/dashboard.html +124 -0
- package/docs/affiliates/index.html +236 -0
- package/docs/agent-install.html +183 -0
- package/docs/ai-agent-security-scanner.html +10 -6
- package/docs/badge/index.html +149 -0
- package/docs/badge/scanning.svg +23 -0
- package/docs/blog/386-malicious-skills.html +262 -0
- package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
- package/docs/blog/agent-trust-protocol.html +198 -0
- package/docs/blog/ai-agent-earns-commissions.html +230 -0
- package/docs/blog/bugmageddon-agent-firewall.html +174 -0
- package/docs/blog/calculator-math.html +180 -0
- package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +229 -0
- package/docs/blog/host-guardian-launch.html +18 -8
- package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
- package/docs/blog/index.html +211 -9
- package/docs/blog/langchain-security-tutorial.html +18 -8
- package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
- package/docs/blog/meta-researcher-rogue-agent.html +201 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
- package/docs/blog/oasis-websocket-hijack.html +212 -0
- package/docs/blog/ollama-openclaw-security.html +160 -0
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
- package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
- package/docs/blog/owasp-agentic-ai-top10.html +18 -8
- package/docs/blog/securing-ai-agents.html +18 -8
- package/docs/blog/supply-chain-agents.html +18 -8
- package/docs/business/index.html +525 -0
- package/docs/business/install.html +261 -0
- package/docs/checklist.html +174 -0
- package/docs/compare/index.html +122 -0
- package/docs/compare/lakera/index.html +62 -0
- package/docs/compare/llm-guard/index.html +49 -0
- package/docs/compare/snyk-agent-scan/index.html +63 -0
- package/docs/compare.html +10 -6
- package/docs/dashboard/index.html +520 -0
- package/docs/finance/index.html +220 -0
- package/docs/guides/business-deployment.html +770 -0
- package/docs/hall-of-fame.html +174 -0
- package/docs/index.html +447 -154
- package/docs/install.sh +557 -0
- package/docs/integrations/langchain.html +14 -6
- package/docs/integrations/openai.html +14 -6
- package/docs/integrations/openclaw.html +55 -7
- package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
- package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
- package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
- package/docs/plans/2026-04-14-v1-release-update.md +91 -0
- package/docs/plans/2026-04-19-supabase-audit.md +68 -0
- package/docs/plans/2026-05-12-sales-push.md +303 -0
- package/docs/playground/index.html +893 -0
- package/docs/playground.html +4 -7
- package/docs/privacy-policy/index.html +122 -0
- package/docs/rfcs/defense-in-depth.md +467 -0
- package/docs/scan/index.html +358 -0
- package/docs/services/case-study.html +255 -0
- package/docs/services/downloads/install-openclaw.bat +45 -0
- package/docs/services/downloads/install-openclaw.command +38 -0
- package/docs/services/downloads/install-openclaw.sh +38 -0
- package/docs/services/get-started.html +165 -0
- package/docs/services/index.html +598 -0
- package/docs/services/multi-agent-security.html +284 -0
- package/docs/services/one-pager.html +99 -0
- package/docs/services/pitch-deck.html +229 -0
- package/docs/services/roi-calculator.html +258 -0
- package/docs/sitemap.xml +192 -2
- package/docs/support/index.html +135 -0
- package/docs/templates/customer-service/HEARTBEAT.md +61 -0
- package/docs/templates/customer-service/MEMORY.md +89 -0
- package/docs/templates/customer-service/SOUL.md +41 -0
- package/docs/templates/customer-service/USER.md +56 -0
- package/docs/templates/executive/HEARTBEAT.md +86 -0
- package/docs/templates/executive/MEMORY.md +92 -0
- package/docs/templates/executive/SOUL.md +44 -0
- package/docs/templates/executive/USER.md +62 -0
- package/docs/templates/finance/HEARTBEAT.md +58 -0
- package/docs/templates/finance/MEMORY.md +87 -0
- package/docs/templates/finance/SOUL.md +38 -0
- package/docs/templates/finance/USER.md +53 -0
- package/docs/templates/index.html +115 -0
- package/docs/templates/operations/HEARTBEAT.md +63 -0
- package/docs/templates/operations/MEMORY.md +68 -0
- package/docs/templates/operations/SOUL.md +38 -0
- package/docs/templates/operations/USER.md +49 -0
- package/docs/templates/sales/HEARTBEAT.md +55 -0
- package/docs/templates/sales/MEMORY.md +89 -0
- package/docs/templates/sales/SOUL.md +34 -0
- package/docs/templates/sales/USER.md +54 -0
- package/docs/terms-of-service/index.html +122 -0
- package/eslint.config.js +32 -0
- package/evals/README.md +29 -0
- package/evals/cases.json +390 -0
- package/evals/results.md +68 -0
- package/evals/run.js +180 -0
- package/examples/basic-usage.js +38 -0
- package/examples/demo-attack/demo.js +186 -0
- package/examples/python-quickstart/README.md +54 -0
- package/examples/python-quickstart/clawmoat_client.py +167 -0
- package/examples/video-demo/README.md +14 -0
- package/examples/video-demo/scene-a-normal.js +29 -0
- package/examples/video-demo/scene-b-attack-arrives.js +31 -0
- package/examples/video-demo/scene-c-hijack.js +44 -0
- package/examples/video-demo/scene-d-clawmoat.js +46 -0
- package/integrations/crewai/README.md +32 -0
- package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
- package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
- package/integrations/crewai/pyproject.toml +21 -0
- package/integrations/langchain/README.md +91 -0
- package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
- package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
- package/integrations/langchain/pyproject.toml +32 -0
- package/integrations/litellm/README.md +324 -0
- package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
- package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
- package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
- package/integrations/litellm/pyproject.toml +74 -0
- package/integrations/openai-agents/README.md +392 -0
- package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
- package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
- package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
- package/integrations/openai-agents/pyproject.toml +76 -0
- package/package.json +6 -5
- package/plugins/openclaw-adapter/PHASE1.md +439 -0
- package/plugins/openclaw-adapter/README.md +103 -0
- package/plugins/openclaw-adapter/SPEC.md +1644 -0
- package/plugins/openclaw-adapter/package.json +31 -0
- package/plugins/openclaw-adapter/src/index.test.ts +226 -0
- package/plugins/openclaw-adapter/src/index.ts +140 -0
- package/plugins/openclaw-adapter/tsconfig.json +14 -0
- package/server/data/threats.json +290 -0
- package/server/index.js +224 -10
- package/src/adapters/express.js +161 -0
- package/src/adapters/index.js +92 -0
- package/src/adapters/langchain.js +185 -0
- package/src/approval/index.js +456 -0
- package/src/ban-scanner.js +200 -0
- package/src/boundary-scanner.js +296 -0
- package/src/ci-scanner.js +279 -0
- package/src/code-scanner.js +245 -0
- package/src/enforce.js +166 -0
- package/src/finance/index.js +585 -0
- package/src/finance/mcp-firewall.js +486 -0
- package/src/formatters/json.js +80 -0
- package/src/formatters/sarif.js +388 -0
- package/src/guardian/alerts.js +34 -3
- package/src/guardian/gateway-monitor.js +590 -0
- package/src/guardian/index.js +41 -2
- package/src/index.js +105 -0
- package/src/integrations/agentmesh.js +501 -0
- package/src/language-detector.js +201 -0
- package/src/mcp-scanner.js +253 -0
- package/src/multimodal/index.js +579 -0
- package/src/obfuscation-scanner.js +457 -0
- package/src/policy-engine.js +402 -0
- package/src/scanners/dependency-attacks.js +128 -0
- package/src/scanners/prompt-injection.js +18 -0
- package/src/scanners/supply-chain.js +14 -0
- package/src/templates/default-config.yml +90 -0
- package/src/vuln-ops/exploitability.js +46 -0
- 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
|
+
}
|