honeymcp 0.1.0__py3-none-any.whl
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.
- honeymcp/__init__.py +34 -0
- honeymcp/cli.py +205 -0
- honeymcp/core/__init__.py +20 -0
- honeymcp/core/dynamic_ghost_tools.py +443 -0
- honeymcp/core/fingerprinter.py +273 -0
- honeymcp/core/ghost_tools.py +624 -0
- honeymcp/core/middleware.py +573 -0
- honeymcp/dashboard/__init__.py +0 -0
- honeymcp/dashboard/app.py +228 -0
- honeymcp/integrations/__init__.py +3 -0
- honeymcp/llm/__init__.py +6 -0
- honeymcp/llm/analyzers.py +278 -0
- honeymcp/llm/clients/__init__.py +102 -0
- honeymcp/llm/clients/provider_type.py +11 -0
- honeymcp/llm/prompts/__init__.py +81 -0
- honeymcp/llm/prompts/dynamic_ghost_tools.yaml +88 -0
- honeymcp/models/__init__.py +8 -0
- honeymcp/models/config.py +187 -0
- honeymcp/models/events.py +60 -0
- honeymcp/models/ghost_tool_spec.py +31 -0
- honeymcp/models/protection_mode.py +17 -0
- honeymcp/storage/__init__.py +5 -0
- honeymcp/storage/event_store.py +176 -0
- honeymcp-0.1.0.dist-info/METADATA +699 -0
- honeymcp-0.1.0.dist-info/RECORD +28 -0
- honeymcp-0.1.0.dist-info/WHEEL +4 -0
- honeymcp-0.1.0.dist-info/entry_points.txt +2 -0
- honeymcp-0.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Attack fingerprinting - capture complete attack context."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from honeymcp.models.events import AttackFingerprint
|
|
7
|
+
from honeymcp.models.ghost_tool_spec import GhostToolSpec
|
|
8
|
+
|
|
9
|
+
# Global session state tracking
|
|
10
|
+
_session_tool_history: Dict[str, List[str]] = {}
|
|
11
|
+
_attacker_detected: Dict[str, bool] = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def mark_attacker_detected(session_id: str) -> None:
|
|
15
|
+
"""Mark a session as having triggered a ghost tool (attacker detected)."""
|
|
16
|
+
_attacker_detected[session_id] = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_attacker_detected(session_id: str) -> bool:
|
|
20
|
+
"""Check if this session has been flagged as an attacker."""
|
|
21
|
+
return _attacker_detected.get(session_id, False)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def record_tool_call(session_id: str, tool_name: str) -> None:
|
|
25
|
+
"""Record a tool call in the session history."""
|
|
26
|
+
if session_id not in _session_tool_history:
|
|
27
|
+
_session_tool_history[session_id] = []
|
|
28
|
+
_session_tool_history[session_id].append(tool_name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_session_tool_history(session_id: str) -> List[str]:
|
|
32
|
+
"""Get the tool call history for a session."""
|
|
33
|
+
return _session_tool_history.get(session_id, [])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def fingerprint_attack(
|
|
37
|
+
tool_name: str,
|
|
38
|
+
arguments: Dict[str, Any],
|
|
39
|
+
context: Any,
|
|
40
|
+
ghost_spec: GhostToolSpec,
|
|
41
|
+
) -> AttackFingerprint:
|
|
42
|
+
"""Capture complete attack context when a ghost tool is triggered.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
tool_name: Name of the ghost tool that was called
|
|
46
|
+
arguments: Arguments passed to the ghost tool
|
|
47
|
+
context: MCP context object (varies by framework)
|
|
48
|
+
ghost_spec: Specification of the triggered ghost tool
|
|
49
|
+
Returns:
|
|
50
|
+
Complete attack fingerprint with all available context
|
|
51
|
+
"""
|
|
52
|
+
# Extract session ID from context
|
|
53
|
+
session_id = _extract_session_id(context)
|
|
54
|
+
|
|
55
|
+
# Get tool call history
|
|
56
|
+
tool_history = get_session_tool_history(session_id)
|
|
57
|
+
|
|
58
|
+
# Try to extract conversation history (may not be available in MCP)
|
|
59
|
+
conversation = _extract_conversation_history(context)
|
|
60
|
+
|
|
61
|
+
# Extract client metadata
|
|
62
|
+
client_metadata = _extract_client_metadata(context)
|
|
63
|
+
|
|
64
|
+
# Generate fake response
|
|
65
|
+
fake_response = ghost_spec.response_generator(arguments)
|
|
66
|
+
|
|
67
|
+
# Create unique event ID
|
|
68
|
+
event_id = f"evt_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{uuid4().hex[:8]}"
|
|
69
|
+
|
|
70
|
+
return AttackFingerprint(
|
|
71
|
+
event_id=event_id,
|
|
72
|
+
timestamp=datetime.utcnow(),
|
|
73
|
+
session_id=session_id,
|
|
74
|
+
ghost_tool_called=tool_name,
|
|
75
|
+
arguments=arguments,
|
|
76
|
+
conversation_history=conversation,
|
|
77
|
+
tool_call_sequence=tool_history,
|
|
78
|
+
threat_level=ghost_spec.threat_level,
|
|
79
|
+
attack_category=ghost_spec.attack_category,
|
|
80
|
+
client_metadata=client_metadata,
|
|
81
|
+
response_sent=fake_response,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_session_id(context: Any) -> str:
|
|
86
|
+
"""Resolve or generate a session ID from MCP context."""
|
|
87
|
+
return _extract_session_id(context)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _extract_session_id(context: Any) -> str:
|
|
91
|
+
"""Extract session ID from MCP context."""
|
|
92
|
+
# Try FastMCP HTTP helper (works even when request_context is missing)
|
|
93
|
+
try:
|
|
94
|
+
from fastmcp.server.dependencies import get_http_request
|
|
95
|
+
|
|
96
|
+
request = get_http_request()
|
|
97
|
+
if request is not None and hasattr(request, "query_params"):
|
|
98
|
+
try:
|
|
99
|
+
value = request.query_params.get("session_id")
|
|
100
|
+
except Exception:
|
|
101
|
+
value = None
|
|
102
|
+
if value:
|
|
103
|
+
return str(value)
|
|
104
|
+
if request is not None and hasattr(request, "headers"):
|
|
105
|
+
try:
|
|
106
|
+
value = request.headers.get("mcp-session-id")
|
|
107
|
+
except Exception:
|
|
108
|
+
value = None
|
|
109
|
+
if value:
|
|
110
|
+
return str(value)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# Try FastMCP middleware context first
|
|
115
|
+
if hasattr(context, "fastmcp_context"):
|
|
116
|
+
fast_ctx = getattr(context, "fastmcp_context")
|
|
117
|
+
if fast_ctx is not None:
|
|
118
|
+
# Prefer HTTP query param session_id when present (e.g., /messages?session_id=...)
|
|
119
|
+
req_ctx = getattr(fast_ctx, "request_context", None)
|
|
120
|
+
if req_ctx is not None:
|
|
121
|
+
request = getattr(req_ctx, "request", None)
|
|
122
|
+
if request is not None and hasattr(request, "query_params"):
|
|
123
|
+
try:
|
|
124
|
+
value = request.query_params.get("session_id")
|
|
125
|
+
except Exception:
|
|
126
|
+
value = None
|
|
127
|
+
if value:
|
|
128
|
+
return str(value)
|
|
129
|
+
if request is not None and hasattr(request, "headers"):
|
|
130
|
+
try:
|
|
131
|
+
value = request.headers.get("mcp-session-id")
|
|
132
|
+
except Exception:
|
|
133
|
+
value = None
|
|
134
|
+
if value:
|
|
135
|
+
return str(value)
|
|
136
|
+
|
|
137
|
+
# Fall back to FastMCP context session_id if available
|
|
138
|
+
if hasattr(fast_ctx, "session_id"):
|
|
139
|
+
try:
|
|
140
|
+
return str(fast_ctx.session_id)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Try direct attributes on the context
|
|
145
|
+
for attr in ["session_id", "id", "request_id", "connection_id"]:
|
|
146
|
+
if hasattr(context, attr):
|
|
147
|
+
value = getattr(context, attr)
|
|
148
|
+
if value:
|
|
149
|
+
return str(value)
|
|
150
|
+
|
|
151
|
+
# Try dict-like contexts
|
|
152
|
+
if isinstance(context, dict):
|
|
153
|
+
for key in ["session_id", "id", "request_id", "connection_id"]:
|
|
154
|
+
value = context.get(key)
|
|
155
|
+
if value:
|
|
156
|
+
return str(value)
|
|
157
|
+
|
|
158
|
+
# Try request object for query/path params or headers (FastMCP/Starlette)
|
|
159
|
+
if hasattr(context, "request"):
|
|
160
|
+
request = getattr(context, "request")
|
|
161
|
+
if request is not None:
|
|
162
|
+
if hasattr(request, "query_params"):
|
|
163
|
+
qp = getattr(request, "query_params")
|
|
164
|
+
try:
|
|
165
|
+
value = qp.get("session_id")
|
|
166
|
+
except Exception:
|
|
167
|
+
value = None
|
|
168
|
+
if value:
|
|
169
|
+
return str(value)
|
|
170
|
+
if hasattr(request, "path_params"):
|
|
171
|
+
pp = getattr(request, "path_params")
|
|
172
|
+
try:
|
|
173
|
+
value = pp.get("session_id")
|
|
174
|
+
except Exception:
|
|
175
|
+
value = None
|
|
176
|
+
if value:
|
|
177
|
+
return str(value)
|
|
178
|
+
if hasattr(request, "headers"):
|
|
179
|
+
headers = getattr(request, "headers")
|
|
180
|
+
try:
|
|
181
|
+
value = (
|
|
182
|
+
headers.get("x-session-id")
|
|
183
|
+
or headers.get("session-id")
|
|
184
|
+
or headers.get("x-mcp-session-id")
|
|
185
|
+
)
|
|
186
|
+
except Exception:
|
|
187
|
+
value = None
|
|
188
|
+
if value:
|
|
189
|
+
return str(value)
|
|
190
|
+
|
|
191
|
+
# Fallback: generate a session ID
|
|
192
|
+
return f"sess_{uuid4().hex[:12]}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _extract_conversation_history(context: Any) -> Optional[List[Dict]]:
|
|
196
|
+
"""Extract conversation history from context if available.
|
|
197
|
+
|
|
198
|
+
Note: MCP protocol may not provide conversation history to tools.
|
|
199
|
+
This is a limitation of the protocol, not HoneyMCP.
|
|
200
|
+
"""
|
|
201
|
+
if hasattr(context, "conversation_history"):
|
|
202
|
+
return getattr(context, "conversation_history")
|
|
203
|
+
|
|
204
|
+
if hasattr(context, "messages"):
|
|
205
|
+
return getattr(context, "messages")
|
|
206
|
+
|
|
207
|
+
# Not available
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _extract_client_metadata(context: Any) -> Dict[str, Any]:
|
|
212
|
+
"""Extract available client metadata from context."""
|
|
213
|
+
metadata = {}
|
|
214
|
+
|
|
215
|
+
# Try FastMCP HTTP helper for request info
|
|
216
|
+
try:
|
|
217
|
+
from fastmcp.server.dependencies import get_http_request
|
|
218
|
+
|
|
219
|
+
request = get_http_request()
|
|
220
|
+
if request is not None:
|
|
221
|
+
if hasattr(request, "headers"):
|
|
222
|
+
try:
|
|
223
|
+
metadata["headers"] = dict(request.headers)
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
if hasattr(request, "client") and request.client:
|
|
227
|
+
try:
|
|
228
|
+
metadata["client_ip"] = request.client.host
|
|
229
|
+
metadata["client_port"] = request.client.port
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
# FastMCP middleware context -> request info
|
|
236
|
+
if hasattr(context, "fastmcp_context"):
|
|
237
|
+
fast_ctx = getattr(context, "fastmcp_context")
|
|
238
|
+
if fast_ctx is not None:
|
|
239
|
+
req_ctx = getattr(fast_ctx, "request_context", None)
|
|
240
|
+
if req_ctx is not None:
|
|
241
|
+
request = getattr(req_ctx, "request", None)
|
|
242
|
+
if request is not None:
|
|
243
|
+
if hasattr(request, "headers"):
|
|
244
|
+
try:
|
|
245
|
+
metadata["headers"] = dict(request.headers)
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
248
|
+
if hasattr(request, "client") and request.client:
|
|
249
|
+
try:
|
|
250
|
+
metadata["client_ip"] = request.client.host
|
|
251
|
+
metadata["client_port"] = request.client.port
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
# Try to extract user agent
|
|
256
|
+
if hasattr(context, "user_agent"):
|
|
257
|
+
metadata["user_agent"] = getattr(context, "user_agent")
|
|
258
|
+
|
|
259
|
+
# Try to extract client info
|
|
260
|
+
if hasattr(context, "client_info"):
|
|
261
|
+
metadata["client_info"] = getattr(context, "client_info")
|
|
262
|
+
|
|
263
|
+
# Try to extract request headers
|
|
264
|
+
if hasattr(context, "headers"):
|
|
265
|
+
headers = getattr(context, "headers")
|
|
266
|
+
if isinstance(headers, dict):
|
|
267
|
+
metadata["headers"] = headers
|
|
268
|
+
|
|
269
|
+
# If no metadata found, return minimal info
|
|
270
|
+
if not metadata:
|
|
271
|
+
metadata["user_agent"] = "unknown"
|
|
272
|
+
|
|
273
|
+
return metadata
|