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.
@@ -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