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,324 @@
|
|
|
1
|
+
# clawmoat-litellm
|
|
2
|
+
|
|
3
|
+
Security scanning middleware for LiteLLM proxy — transparent protection for all models and providers without application changes.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install clawmoat-litellm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Option 1: LiteLLM Config File
|
|
14
|
+
|
|
15
|
+
Add to your `litellm_config.yaml`:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
model_list:
|
|
19
|
+
- model_name: gpt-4
|
|
20
|
+
provider: openai
|
|
21
|
+
- model_name: claude-3
|
|
22
|
+
provider: anthropic
|
|
23
|
+
|
|
24
|
+
litellm_settings:
|
|
25
|
+
callbacks: ["clawmoat_litellm.ClawMoatCallback"]
|
|
26
|
+
|
|
27
|
+
general_settings:
|
|
28
|
+
# ClawMoat configuration
|
|
29
|
+
clawmoat:
|
|
30
|
+
block_on_critical: true
|
|
31
|
+
block_on_high: false
|
|
32
|
+
scan_input: true
|
|
33
|
+
scan_output: true
|
|
34
|
+
base_url: "http://localhost:8080" # Optional: ClawMoat server
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Option 2: Programmatic Setup
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import litellm
|
|
41
|
+
from clawmoat_litellm import ClawMoatCallback
|
|
42
|
+
|
|
43
|
+
# Add ClawMoat as a callback
|
|
44
|
+
litellm.callbacks = [ClawMoatCallback(block_on_critical=True)]
|
|
45
|
+
|
|
46
|
+
# Now all LLM calls are protected
|
|
47
|
+
response = litellm.completion(
|
|
48
|
+
model="gpt-4",
|
|
49
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Option 3: Proxy Deployment
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from litellm import proxy
|
|
57
|
+
from clawmoat_litellm import ClawMoatProxyMiddleware
|
|
58
|
+
|
|
59
|
+
# Add security middleware to proxy
|
|
60
|
+
middleware = ClawMoatProxyMiddleware(
|
|
61
|
+
block_on_critical=True,
|
|
62
|
+
log_all_requests=True
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Register hooks
|
|
66
|
+
proxy.register_pre_call_hook(middleware.pre_call_hook)
|
|
67
|
+
proxy.register_post_call_hook(middleware.post_call_hook)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What It Protects
|
|
71
|
+
|
|
72
|
+
| Attack Vector | Detection | Action |
|
|
73
|
+
|---------------|-----------|--------|
|
|
74
|
+
| **Prompt Injection** | Pattern matching, intent analysis | Block or log |
|
|
75
|
+
| **Jailbreak Attempts** | Known attack signatures | Block or log |
|
|
76
|
+
| **PII/Secrets in Input** | Regex + ML patterns | Block or redact |
|
|
77
|
+
| **Data Exfiltration** | Output scanning for credentials | Log and optionally redact |
|
|
78
|
+
| **Excessive Agency** | Privilege escalation patterns | Block |
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
### ClawMoat Server Mode
|
|
83
|
+
|
|
84
|
+
For full scanning capabilities, run a ClawMoat server:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Start ClawMoat server
|
|
88
|
+
clawmoat serve --port 8080
|
|
89
|
+
|
|
90
|
+
# Configure LiteLLM to use it
|
|
91
|
+
litellm.callbacks = [ClawMoatCallback(
|
|
92
|
+
base_url="http://localhost:8080",
|
|
93
|
+
api_key="your-api-key"
|
|
94
|
+
)]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Local Mode
|
|
98
|
+
|
|
99
|
+
For lightweight scanning without external dependencies:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
callback = ClawMoatCallback(
|
|
103
|
+
base_url=None, # Use local scanning
|
|
104
|
+
block_on_critical=True
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Advanced Configuration
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
callback = ClawMoatCallback(
|
|
112
|
+
# Security settings
|
|
113
|
+
block_on_critical=True,
|
|
114
|
+
block_on_high=False,
|
|
115
|
+
scan_input=True,
|
|
116
|
+
scan_output=True,
|
|
117
|
+
|
|
118
|
+
# Performance settings
|
|
119
|
+
timeout=5, # API timeout seconds
|
|
120
|
+
fallback_mode="allow", # "allow" | "block" on errors
|
|
121
|
+
|
|
122
|
+
# Remote server (optional)
|
|
123
|
+
base_url="http://localhost:8080",
|
|
124
|
+
api_key="sk-clawmoat-...",
|
|
125
|
+
|
|
126
|
+
# Debugging
|
|
127
|
+
verbose=True
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Monitoring & Stats
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# Access scanning statistics
|
|
135
|
+
print(callback.stats)
|
|
136
|
+
# {
|
|
137
|
+
# "requests_scanned": 156,
|
|
138
|
+
# "threats_detected": 3,
|
|
139
|
+
# "requests_blocked": 1,
|
|
140
|
+
# "fallbacks": 0
|
|
141
|
+
# }
|
|
142
|
+
|
|
143
|
+
# View detected threats
|
|
144
|
+
for finding in callback.findings:
|
|
145
|
+
print(f"{finding['type']}: {finding['description']}")
|
|
146
|
+
|
|
147
|
+
# For proxy middleware
|
|
148
|
+
middleware = ClawMoatProxyMiddleware()
|
|
149
|
+
print(middleware.get_stats())
|
|
150
|
+
print(middleware.get_recent_blocks(limit=5))
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Enterprise Features
|
|
154
|
+
|
|
155
|
+
### Audit Logging
|
|
156
|
+
|
|
157
|
+
All security events are logged for compliance:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
callback = ClawMoatCallback(
|
|
161
|
+
base_url="http://your-clawmoat-server.com",
|
|
162
|
+
verbose=True # Enables audit logging
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Logs include:
|
|
166
|
+
# - Request/response content (configurable)
|
|
167
|
+
# - Security findings and severity
|
|
168
|
+
# - Block/allow decisions
|
|
169
|
+
# - User identifiers and timestamps
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Custom Policies
|
|
173
|
+
|
|
174
|
+
Define organization-specific security policies:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
# clawmoat_policy.yaml
|
|
178
|
+
policies:
|
|
179
|
+
- name: "block_competitor_research"
|
|
180
|
+
pattern: ".*competitor.*financial.*data.*"
|
|
181
|
+
action: "block"
|
|
182
|
+
severity: "high"
|
|
183
|
+
|
|
184
|
+
- name: "redact_customer_pii"
|
|
185
|
+
pattern: "\\b\\d{3}-\\d{2}-\\d{4}\\b" # SSN
|
|
186
|
+
action: "redact"
|
|
187
|
+
replacement: "[SSN_REDACTED]"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Multi-Tenant Support
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# Different policies per user/tenant
|
|
194
|
+
callback = ClawMoatCallback(
|
|
195
|
+
policy_selector=lambda user_id: f"policies/{user_id}.yaml"
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Integration Examples
|
|
200
|
+
|
|
201
|
+
### FastAPI + LiteLLM
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from fastapi import FastAPI
|
|
205
|
+
import litellm
|
|
206
|
+
from clawmoat_litellm import ClawMoatCallback
|
|
207
|
+
|
|
208
|
+
app = FastAPI()
|
|
209
|
+
|
|
210
|
+
# Configure security
|
|
211
|
+
litellm.callbacks = [ClawMoatCallback(block_on_critical=True)]
|
|
212
|
+
|
|
213
|
+
@app.post("/chat")
|
|
214
|
+
async def chat_endpoint(message: str):
|
|
215
|
+
try:
|
|
216
|
+
response = litellm.completion(
|
|
217
|
+
model="gpt-4",
|
|
218
|
+
messages=[{"role": "user", "content": message}]
|
|
219
|
+
)
|
|
220
|
+
return {"response": response.choices[0].message.content}
|
|
221
|
+
except ValueError as e:
|
|
222
|
+
# ClawMoat blocked the request
|
|
223
|
+
return {"error": "Request blocked by security system", "reason": str(e)}, 403
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### LangChain via LiteLLM
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from langchain_community.llms import LiteLLM
|
|
230
|
+
from clawmoat_litellm import ClawMoatCallback
|
|
231
|
+
|
|
232
|
+
# Add security to LangChain via LiteLLM
|
|
233
|
+
litellm.callbacks = [ClawMoatCallback()]
|
|
234
|
+
|
|
235
|
+
llm = LiteLLM(model="gpt-4")
|
|
236
|
+
response = llm("What is the capital of France?") # Protected automatically
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Kubernetes Deployment
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
# k8s-litellm-clawmoat.yaml
|
|
243
|
+
apiVersion: apps/v1
|
|
244
|
+
kind: Deployment
|
|
245
|
+
metadata:
|
|
246
|
+
name: litellm-proxy-secure
|
|
247
|
+
spec:
|
|
248
|
+
template:
|
|
249
|
+
spec:
|
|
250
|
+
containers:
|
|
251
|
+
- name: litellm
|
|
252
|
+
image: ghcr.io/berriai/litellm:main-latest
|
|
253
|
+
env:
|
|
254
|
+
- name: LITELLM_MASTER_KEY
|
|
255
|
+
value: "sk-1234"
|
|
256
|
+
volumeMounts:
|
|
257
|
+
- name: config
|
|
258
|
+
mountPath: /app/config.yaml
|
|
259
|
+
subPath: config.yaml
|
|
260
|
+
volumes:
|
|
261
|
+
- name: config
|
|
262
|
+
configMap:
|
|
263
|
+
name: litellm-config
|
|
264
|
+
---
|
|
265
|
+
apiVersion: v1
|
|
266
|
+
kind: ConfigMap
|
|
267
|
+
metadata:
|
|
268
|
+
name: litellm-config
|
|
269
|
+
data:
|
|
270
|
+
config.yaml: |
|
|
271
|
+
model_list:
|
|
272
|
+
- model_name: gpt-4
|
|
273
|
+
provider: openai
|
|
274
|
+
litellm_settings:
|
|
275
|
+
callbacks: ["clawmoat_litellm.ClawMoatCallback"]
|
|
276
|
+
general_settings:
|
|
277
|
+
clawmoat:
|
|
278
|
+
block_on_critical: true
|
|
279
|
+
base_url: "http://clawmoat-service:8080"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Performance
|
|
283
|
+
|
|
284
|
+
- **Latency Impact:** ~10-50ms per request (local mode) / ~20-100ms (server mode)
|
|
285
|
+
- **Memory Usage:** ~5-20MB additional depending on findings cache size
|
|
286
|
+
- **Throughput:** Negligible impact on proxy throughput
|
|
287
|
+
- **Scaling:** Each callback instance maintains independent state
|
|
288
|
+
|
|
289
|
+
## Troubleshooting
|
|
290
|
+
|
|
291
|
+
### Common Issues
|
|
292
|
+
|
|
293
|
+
**ClawMoat server unreachable:**
|
|
294
|
+
```python
|
|
295
|
+
callback = ClawMoatCallback(
|
|
296
|
+
base_url="http://localhost:8080",
|
|
297
|
+
fallback_mode="allow", # Don't block when server is down
|
|
298
|
+
timeout=2 # Shorter timeout
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Too many false positives:**
|
|
303
|
+
```python
|
|
304
|
+
callback = ClawMoatCallback(
|
|
305
|
+
block_on_critical=True,
|
|
306
|
+
block_on_high=False, # Only block critical threats
|
|
307
|
+
verbose=True # See what's being detected
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Performance concerns:**
|
|
312
|
+
```python
|
|
313
|
+
callback = ClawMoatCallback(
|
|
314
|
+
scan_input=True,
|
|
315
|
+
scan_output=False, # Skip output scanning for speed
|
|
316
|
+
timeout=1 # Aggressive timeout
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Links
|
|
321
|
+
|
|
322
|
+
- [ClawMoat](https://github.com/darfaz/clawmoat) — Open-source runtime security for AI agents
|
|
323
|
+
- [LiteLLM](https://github.com/BerriAI/litellm) — Universal LLM proxy
|
|
324
|
+
- [clawmoat-langchain](../langchain/) — Direct LangChain integration
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""ClawMoat security integration for LiteLLM proxy.
|
|
2
|
+
|
|
3
|
+
Provides transparent security scanning for all models and providers
|
|
4
|
+
behind a LiteLLM proxy gateway without requiring application changes.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# In litellm_config.yaml
|
|
8
|
+
litellm_settings:
|
|
9
|
+
callbacks: ["clawmoat_litellm.ClawMoatCallback"]
|
|
10
|
+
|
|
11
|
+
# Or programmatically
|
|
12
|
+
from clawmoat_litellm import ClawMoatCallback
|
|
13
|
+
import litellm
|
|
14
|
+
litellm.callbacks = [ClawMoatCallback()]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from clawmoat_litellm.callback import ClawMoatCallback
|
|
18
|
+
from clawmoat_litellm.proxy_middleware import ClawMoatProxyMiddleware
|
|
19
|
+
|
|
20
|
+
__all__ = ["ClawMoatCallback", "ClawMoatProxyMiddleware"]
|
|
21
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""LiteLLM callback handler for ClawMoat security scanning."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Optional, Dict, Any, List, Union
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import litellm
|
|
10
|
+
from litellm.integrations.custom_logger import CustomLogger
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ImportError("litellm is required. Install with: pip install litellm")
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import requests
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise ImportError("requests is required. Install with: pip install requests")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClawMoatCallback(CustomLogger):
|
|
21
|
+
"""LiteLLM custom callback for ClawMoat security scanning.
|
|
22
|
+
|
|
23
|
+
Scans all prompts and responses flowing through the LiteLLM proxy
|
|
24
|
+
for prompt injection, PII/secrets, and other security threats.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
base_url: str = "http://localhost:8080",
|
|
30
|
+
api_key: Optional[str] = None,
|
|
31
|
+
block_on_critical: bool = True,
|
|
32
|
+
block_on_high: bool = False,
|
|
33
|
+
scan_input: bool = True,
|
|
34
|
+
scan_output: bool = True,
|
|
35
|
+
timeout: int = 5,
|
|
36
|
+
fallback_mode: str = "allow", # "allow" | "block"
|
|
37
|
+
verbose: bool = False
|
|
38
|
+
):
|
|
39
|
+
"""Initialize ClawMoat callback.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
base_url: ClawMoat server URL (or use local scanning)
|
|
43
|
+
api_key: Authentication for ClawMoat server
|
|
44
|
+
block_on_critical: Block requests with critical threats
|
|
45
|
+
block_on_high: Block requests with high severity threats
|
|
46
|
+
scan_input: Scan prompts and messages
|
|
47
|
+
scan_output: Scan LLM responses
|
|
48
|
+
timeout: Timeout for ClawMoat API calls (seconds)
|
|
49
|
+
fallback_mode: What to do when ClawMoat is unreachable
|
|
50
|
+
verbose: Enable debug logging
|
|
51
|
+
"""
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.base_url = base_url.rstrip('/')
|
|
54
|
+
self.api_key = api_key
|
|
55
|
+
self.block_on_critical = block_on_critical
|
|
56
|
+
self.block_on_high = block_on_high
|
|
57
|
+
self.scan_input = scan_input
|
|
58
|
+
self.scan_output = scan_output
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
self.fallback_mode = fallback_mode
|
|
61
|
+
self.verbose = verbose
|
|
62
|
+
|
|
63
|
+
self.findings = []
|
|
64
|
+
self.stats = {
|
|
65
|
+
"requests_scanned": 0,
|
|
66
|
+
"threats_detected": 0,
|
|
67
|
+
"requests_blocked": 0,
|
|
68
|
+
"fallbacks": 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def log_pre_api_call(self, model: str, messages: List[Dict[str, Any]], kwargs: Dict[str, Any]) -> None:
|
|
72
|
+
"""Called before LLM API call — scan input for threats."""
|
|
73
|
+
if not self.scan_input:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
self.stats["requests_scanned"] += 1
|
|
78
|
+
|
|
79
|
+
# Extract text from messages
|
|
80
|
+
text_content = self._extract_text_from_messages(messages)
|
|
81
|
+
|
|
82
|
+
# Scan content
|
|
83
|
+
scan_result = self._scan_content(text_content, scan_type="inbound")
|
|
84
|
+
|
|
85
|
+
if scan_result and scan_result.get("findings"):
|
|
86
|
+
self.findings.extend(scan_result["findings"])
|
|
87
|
+
self.stats["threats_detected"] += len(scan_result["findings"])
|
|
88
|
+
|
|
89
|
+
# Check if we should block
|
|
90
|
+
if self._should_block(scan_result["findings"]):
|
|
91
|
+
self.stats["requests_blocked"] += 1
|
|
92
|
+
self._log(f"BLOCKED request to {model}: {len(scan_result['findings'])} threats detected")
|
|
93
|
+
|
|
94
|
+
# Create a detailed error message
|
|
95
|
+
threat_summary = self._format_threat_summary(scan_result["findings"])
|
|
96
|
+
raise ValueError(f"ClawMoat blocked request due to security threats: {threat_summary}")
|
|
97
|
+
|
|
98
|
+
else:
|
|
99
|
+
self._log(f"WARNING: {len(scan_result['findings'])} non-blocking threats detected for {model}")
|
|
100
|
+
|
|
101
|
+
except ValueError:
|
|
102
|
+
# Re-raise blocking errors
|
|
103
|
+
raise
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Handle scanning errors based on fallback mode
|
|
106
|
+
self.stats["fallbacks"] += 1
|
|
107
|
+
self._log(f"ClawMoat scanning error: {e}")
|
|
108
|
+
|
|
109
|
+
if self.fallback_mode == "block":
|
|
110
|
+
raise ValueError(f"ClawMoat scanning failed (fallback=block): {e}")
|
|
111
|
+
# Otherwise, continue with "allow" fallback
|
|
112
|
+
|
|
113
|
+
def log_success_event(self, kwargs: Dict[str, Any], response_obj: Any, start_time: datetime, end_time: datetime) -> None:
|
|
114
|
+
"""Called after successful LLM response — scan output for threats."""
|
|
115
|
+
if not self.scan_output:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Extract response content
|
|
120
|
+
response_text = self._extract_response_text(response_obj)
|
|
121
|
+
if not response_text:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Scan response
|
|
125
|
+
scan_result = self._scan_content(response_text, scan_type="outbound")
|
|
126
|
+
|
|
127
|
+
if scan_result and scan_result.get("findings"):
|
|
128
|
+
self.findings.extend(scan_result["findings"])
|
|
129
|
+
self.stats["threats_detected"] += len(scan_result["findings"])
|
|
130
|
+
|
|
131
|
+
# Log findings (output scanning is typically non-blocking for UX)
|
|
132
|
+
self._log(f"Output scan: {len(scan_result['findings'])} findings in response")
|
|
133
|
+
|
|
134
|
+
# Could implement response filtering/redaction here
|
|
135
|
+
# For now, just log the findings
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.stats["fallbacks"] += 1
|
|
139
|
+
self._log(f"ClawMoat output scanning error: {e}")
|
|
140
|
+
|
|
141
|
+
def log_failure_event(self, kwargs: Dict[str, Any], response_obj: Any, start_time: datetime, end_time: datetime) -> None:
|
|
142
|
+
"""Called when LLM call fails — log for audit."""
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def _extract_text_from_messages(self, messages: List[Dict[str, Any]]) -> str:
|
|
146
|
+
"""Extract text content from messages list."""
|
|
147
|
+
texts = []
|
|
148
|
+
for message in messages:
|
|
149
|
+
if isinstance(message, dict):
|
|
150
|
+
content = message.get("content", "")
|
|
151
|
+
if isinstance(content, str):
|
|
152
|
+
texts.append(content)
|
|
153
|
+
elif isinstance(content, list):
|
|
154
|
+
# Handle structured content (images, etc.)
|
|
155
|
+
for item in content:
|
|
156
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
157
|
+
texts.append(item.get("text", ""))
|
|
158
|
+
elif isinstance(message, str):
|
|
159
|
+
texts.append(message)
|
|
160
|
+
|
|
161
|
+
return "\n".join(texts)
|
|
162
|
+
|
|
163
|
+
def _extract_response_text(self, response_obj: Any) -> str:
|
|
164
|
+
"""Extract text content from LLM response."""
|
|
165
|
+
try:
|
|
166
|
+
# Handle different response formats
|
|
167
|
+
if hasattr(response_obj, 'choices') and response_obj.choices:
|
|
168
|
+
choice = response_obj.choices[0]
|
|
169
|
+
if hasattr(choice, 'message') and hasattr(choice.message, 'content'):
|
|
170
|
+
return choice.message.content or ""
|
|
171
|
+
elif hasattr(choice, 'text'):
|
|
172
|
+
return choice.text or ""
|
|
173
|
+
|
|
174
|
+
# Fallback: try to extract from dict representation
|
|
175
|
+
if hasattr(response_obj, 'model_dump'):
|
|
176
|
+
data = response_obj.model_dump()
|
|
177
|
+
elif hasattr(response_obj, 'dict'):
|
|
178
|
+
data = response_obj.dict()
|
|
179
|
+
elif isinstance(response_obj, dict):
|
|
180
|
+
data = response_obj
|
|
181
|
+
else:
|
|
182
|
+
return ""
|
|
183
|
+
|
|
184
|
+
# Try to find content in nested structure
|
|
185
|
+
choices = data.get('choices', [])
|
|
186
|
+
if choices:
|
|
187
|
+
choice = choices[0]
|
|
188
|
+
message = choice.get('message', {})
|
|
189
|
+
return message.get('content', choice.get('text', ''))
|
|
190
|
+
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
self._log(f"Error extracting response text: {e}")
|
|
195
|
+
return ""
|
|
196
|
+
|
|
197
|
+
def _scan_content(self, content: str, scan_type: str = "inbound") -> Optional[Dict[str, Any]]:
|
|
198
|
+
"""Scan content using ClawMoat API or local scanning."""
|
|
199
|
+
if not content.strip():
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
# If we have a base_url, use remote scanning
|
|
204
|
+
if self.base_url and self.base_url != "http://localhost:8080":
|
|
205
|
+
return self._scan_remote(content, scan_type)
|
|
206
|
+
else:
|
|
207
|
+
return self._scan_local(content, scan_type)
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
self._log(f"Scanning error: {e}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _scan_remote(self, content: str, scan_type: str) -> Optional[Dict[str, Any]]:
|
|
214
|
+
"""Scan content using remote ClawMoat server."""
|
|
215
|
+
try:
|
|
216
|
+
headers = {"Content-Type": "application/json"}
|
|
217
|
+
if self.api_key:
|
|
218
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
219
|
+
|
|
220
|
+
endpoint = f"{self.base_url}/scan/{scan_type}"
|
|
221
|
+
payload = {"content": content}
|
|
222
|
+
|
|
223
|
+
response = requests.post(
|
|
224
|
+
endpoint,
|
|
225
|
+
json=payload,
|
|
226
|
+
headers=headers,
|
|
227
|
+
timeout=self.timeout
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if response.status_code == 200:
|
|
231
|
+
return response.json()
|
|
232
|
+
else:
|
|
233
|
+
self._log(f"ClawMoat API error: {response.status_code} - {response.text}")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
except requests.exceptions.RequestException as e:
|
|
237
|
+
self._log(f"ClawMoat API request failed: {e}")
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
def _scan_local(self, content: str, scan_type: str) -> Optional[Dict[str, Any]]:
|
|
241
|
+
"""Simple local scanning using basic patterns."""
|
|
242
|
+
# This is a lightweight fallback when no remote ClawMoat server is available
|
|
243
|
+
# For full capabilities, use a dedicated ClawMoat server
|
|
244
|
+
|
|
245
|
+
findings = []
|
|
246
|
+
|
|
247
|
+
# Basic prompt injection patterns
|
|
248
|
+
injection_patterns = [
|
|
249
|
+
r"ignore\s+(?:all\s+)?previous\s+instructions",
|
|
250
|
+
r"disregard\s+(?:all\s+)?previous\s+instructions",
|
|
251
|
+
r"forget\s+(?:all\s+)?previous\s+instructions",
|
|
252
|
+
r"system\s*:?\s*you\s+are\s+now",
|
|
253
|
+
r"[\/\\]\s*system\s*[\/\\]",
|
|
254
|
+
r"<\s*system\s*>",
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
import re
|
|
258
|
+
for pattern in injection_patterns:
|
|
259
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
260
|
+
findings.append({
|
|
261
|
+
"type": "prompt_injection",
|
|
262
|
+
"severity": "critical",
|
|
263
|
+
"confidence": 0.8,
|
|
264
|
+
"description": "Potential prompt injection detected",
|
|
265
|
+
"pattern": pattern
|
|
266
|
+
})
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
# Basic secrets patterns
|
|
270
|
+
secrets_patterns = [
|
|
271
|
+
(r"sk-[a-zA-Z0-9]{48}", "openai_api_key"),
|
|
272
|
+
(r"ghp_[a-zA-Z0-9]{36}", "github_token"),
|
|
273
|
+
(r"AKIA[0-9A-Z]{16}", "aws_access_key"),
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
for pattern, secret_type in secrets_patterns:
|
|
277
|
+
if re.search(pattern, content):
|
|
278
|
+
findings.append({
|
|
279
|
+
"type": "secrets",
|
|
280
|
+
"subtype": secret_type,
|
|
281
|
+
"severity": "critical",
|
|
282
|
+
"confidence": 0.9,
|
|
283
|
+
"description": f"Potential {secret_type} detected"
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return {"findings": findings} if findings else None
|
|
287
|
+
|
|
288
|
+
def _should_block(self, findings: List[Dict[str, Any]]) -> bool:
|
|
289
|
+
"""Determine if request should be blocked based on findings."""
|
|
290
|
+
for finding in findings:
|
|
291
|
+
severity = finding.get("severity", "low")
|
|
292
|
+
if severity == "critical" and self.block_on_critical:
|
|
293
|
+
return True
|
|
294
|
+
if severity == "high" and self.block_on_high:
|
|
295
|
+
return True
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
def _format_threat_summary(self, findings: List[Dict[str, Any]]) -> str:
|
|
299
|
+
"""Format findings into a readable threat summary."""
|
|
300
|
+
if not findings:
|
|
301
|
+
return "Unknown threat"
|
|
302
|
+
|
|
303
|
+
severities = {}
|
|
304
|
+
for finding in findings:
|
|
305
|
+
severity = finding.get("severity", "unknown")
|
|
306
|
+
severities[severity] = severities.get(severity, 0) + 1
|
|
307
|
+
|
|
308
|
+
parts = []
|
|
309
|
+
for severity in ["critical", "high", "warning", "low"]:
|
|
310
|
+
if severity in severities:
|
|
311
|
+
parts.append(f"{severities[severity]} {severity}")
|
|
312
|
+
|
|
313
|
+
return ", ".join(parts) or "1 unknown"
|
|
314
|
+
|
|
315
|
+
def _log(self, message: str) -> None:
|
|
316
|
+
"""Log message if verbose mode is enabled."""
|
|
317
|
+
if self.verbose:
|
|
318
|
+
print(f"[ClawMoat] {message}")
|
|
319
|
+
|
|
320
|
+
# Additional helper methods for compatibility
|
|
321
|
+
async def async_log_pre_api_call(self, model: str, messages: List[Dict[str, Any]], kwargs: Dict[str, Any]) -> None:
|
|
322
|
+
"""Async version of pre-API call logging."""
|
|
323
|
+
# For now, just call the sync version
|
|
324
|
+
# Could be enhanced with async HTTP requests
|
|
325
|
+
self.log_pre_api_call(model, messages, kwargs)
|
|
326
|
+
|
|
327
|
+
async def async_log_success_event(self, kwargs: Dict[str, Any], response_obj: Any, start_time: datetime, end_time: datetime) -> None:
|
|
328
|
+
"""Async version of success event logging."""
|
|
329
|
+
self.log_success_event(kwargs, response_obj, start_time, end_time)
|