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,573 @@
|
|
|
1
|
+
"""HoneyMCP middleware - one-line integration for FastMCP servers."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
5
|
+
import logging
|
|
6
|
+
import asyncio
|
|
7
|
+
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from fastmcp.tools.tool import ToolResult
|
|
10
|
+
from mcp.types import TextContent
|
|
11
|
+
|
|
12
|
+
from honeymcp.core.fingerprinter import (
|
|
13
|
+
fingerprint_attack,
|
|
14
|
+
record_tool_call,
|
|
15
|
+
mark_attacker_detected,
|
|
16
|
+
is_attacker_detected,
|
|
17
|
+
resolve_session_id,
|
|
18
|
+
)
|
|
19
|
+
from honeymcp.core.ghost_tools import GHOST_TOOL_CATALOG, get_ghost_tool
|
|
20
|
+
from honeymcp.core.dynamic_ghost_tools import DynamicGhostToolGenerator, DynamicGhostToolSpec
|
|
21
|
+
from honeymcp.llm.analyzers import extract_tool_info
|
|
22
|
+
from honeymcp.models.config import HoneyMCPConfig, resolve_event_storage_path
|
|
23
|
+
from honeymcp.models.protection_mode import ProtectionMode
|
|
24
|
+
from honeymcp.storage.event_store import store_event
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def honeypot_from_config(
|
|
30
|
+
server: FastMCP,
|
|
31
|
+
config_path: Optional[Union[str, Path]] = None,
|
|
32
|
+
) -> FastMCP:
|
|
33
|
+
"""Wrap a FastMCP server with HoneyMCP using configuration file.
|
|
34
|
+
|
|
35
|
+
This is an alternative to honeypot() that loads settings from a YAML file.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
from fastmcp import FastMCP
|
|
39
|
+
from honeymcp import honeypot_from_config
|
|
40
|
+
|
|
41
|
+
mcp = FastMCP("My Server")
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
def my_real_tool():
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
mcp = honeypot_from_config(mcp) # Loads from config.yaml
|
|
48
|
+
# or
|
|
49
|
+
mcp = honeypot_from_config(mcp, "path/to/config.yaml")
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
server: FastMCP server instance to wrap
|
|
53
|
+
config_path: Path to YAML config file. If None, searches default locations:
|
|
54
|
+
1. ./config.yaml
|
|
55
|
+
2. ./honeymcp.yaml
|
|
56
|
+
3. ~/.honeymcp/config.yaml
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The wrapped FastMCP server with honeypot capabilities
|
|
60
|
+
"""
|
|
61
|
+
config = HoneyMCPConfig.load(config_path)
|
|
62
|
+
logger.info("Loaded HoneyMCP config: protection_mode=%s", config.protection_mode.value)
|
|
63
|
+
|
|
64
|
+
return honeypot(
|
|
65
|
+
server=server,
|
|
66
|
+
ghost_tools=config.ghost_tools if config.ghost_tools else None,
|
|
67
|
+
use_dynamic_tools=config.use_dynamic_tools,
|
|
68
|
+
num_dynamic_tools=config.num_dynamic_tools,
|
|
69
|
+
llm_model=config.llm_model,
|
|
70
|
+
cache_ttl=config.cache_ttl,
|
|
71
|
+
fallback_to_static=config.fallback_to_static,
|
|
72
|
+
event_storage_path=config.event_storage_path,
|
|
73
|
+
enable_dashboard=config.enable_dashboard,
|
|
74
|
+
protection_mode=config.protection_mode,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def honeypot( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-branches,too-many-statements,too-many-locals,protected-access
|
|
79
|
+
server: FastMCP,
|
|
80
|
+
ghost_tools: Optional[List[str]] = None,
|
|
81
|
+
use_dynamic_tools: bool = True,
|
|
82
|
+
num_dynamic_tools: int = 3,
|
|
83
|
+
llm_model: Optional[str] = None,
|
|
84
|
+
cache_ttl: int = 3600,
|
|
85
|
+
fallback_to_static: bool = True,
|
|
86
|
+
event_storage_path: Optional[Path] = None,
|
|
87
|
+
enable_dashboard: bool = True,
|
|
88
|
+
protection_mode: ProtectionMode = ProtectionMode.SCANNER,
|
|
89
|
+
) -> FastMCP:
|
|
90
|
+
"""Wrap a FastMCP server with HoneyMCP deception capabilities.
|
|
91
|
+
|
|
92
|
+
This decorator injects ghost tools (honeypots) into your MCP server
|
|
93
|
+
and captures detailed attack context when they're triggered.
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
from fastmcp import FastMCP
|
|
97
|
+
from honeymcp import honeypot
|
|
98
|
+
|
|
99
|
+
mcp = FastMCP("My Server")
|
|
100
|
+
|
|
101
|
+
@mcp.tool()
|
|
102
|
+
def my_real_tool():
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
mcp = honeypot(mcp) # One line!
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
server: FastMCP server instance to wrap
|
|
109
|
+
ghost_tools: List of static ghost tool names to inject
|
|
110
|
+
(default: list_cloud_secrets, execute_shell_command)
|
|
111
|
+
use_dynamic_tools: Enable LLM-based dynamic ghost tool generation (default: True)
|
|
112
|
+
num_dynamic_tools: Number of dynamic ghost tools to generate (default: 3)
|
|
113
|
+
llm_model: Override default LLM model for ghost tool generation
|
|
114
|
+
cache_ttl: Cache time-to-live in seconds for generated tools (default: 3600)
|
|
115
|
+
fallback_to_static: Use static ghost tools if dynamic generation fails (default: True)
|
|
116
|
+
event_storage_path: Directory for storing attack events
|
|
117
|
+
(default: ~/.honeymcp/events)
|
|
118
|
+
enable_dashboard: Enable Streamlit dashboard (default: True)
|
|
119
|
+
protection_mode: Protection mode after attacker detection (default: SCANNER)
|
|
120
|
+
- SCANNER: Lockout mode - all tools return errors
|
|
121
|
+
- COGNITIVE: Deception mode - real tools return fake/mock data
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The wrapped FastMCP server with honeypot capabilities
|
|
125
|
+
"""
|
|
126
|
+
# Build configuration
|
|
127
|
+
config = HoneyMCPConfig(
|
|
128
|
+
ghost_tools=ghost_tools or [],
|
|
129
|
+
use_dynamic_tools=use_dynamic_tools,
|
|
130
|
+
num_dynamic_tools=num_dynamic_tools,
|
|
131
|
+
llm_model=llm_model,
|
|
132
|
+
cache_ttl=cache_ttl,
|
|
133
|
+
fallback_to_static=fallback_to_static,
|
|
134
|
+
event_storage_path=resolve_event_storage_path(event_storage_path),
|
|
135
|
+
enable_dashboard=enable_dashboard,
|
|
136
|
+
protection_mode=protection_mode,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Track ghost tool names for quick lookup
|
|
140
|
+
ghost_tool_names = set()
|
|
141
|
+
|
|
142
|
+
# Store dynamic ghost tool specs for later use
|
|
143
|
+
dynamic_ghost_specs = {}
|
|
144
|
+
|
|
145
|
+
# Store mock responses for real tools (used in COGNITIVE protection mode)
|
|
146
|
+
real_tool_mocks: Dict[str, str] = {}
|
|
147
|
+
|
|
148
|
+
# 1. Inject static ghost tools (if specified)
|
|
149
|
+
if ghost_tools:
|
|
150
|
+
logger.info("Registering %s static ghost tools", len(ghost_tools))
|
|
151
|
+
for tool_name in ghost_tools:
|
|
152
|
+
if tool_name not in GHOST_TOOL_CATALOG:
|
|
153
|
+
raise ValueError(f"Unknown static ghost tool: {tool_name}")
|
|
154
|
+
|
|
155
|
+
ghost_spec = get_ghost_tool(tool_name)
|
|
156
|
+
_register_ghost_tool(server, ghost_spec)
|
|
157
|
+
ghost_tool_names.add(tool_name)
|
|
158
|
+
|
|
159
|
+
# 2. Generate and inject dynamic ghost tools (if enabled)
|
|
160
|
+
if use_dynamic_tools:
|
|
161
|
+
try:
|
|
162
|
+
logger.info("Initializing dynamic ghost tool generation")
|
|
163
|
+
|
|
164
|
+
# Initialize LLM-based generator
|
|
165
|
+
generator = DynamicGhostToolGenerator(cache_ttl=cache_ttl, model_name=llm_model)
|
|
166
|
+
|
|
167
|
+
# Run async operations in event loop
|
|
168
|
+
try:
|
|
169
|
+
loop = asyncio.get_event_loop()
|
|
170
|
+
except RuntimeError:
|
|
171
|
+
loop = asyncio.new_event_loop()
|
|
172
|
+
asyncio.set_event_loop(loop)
|
|
173
|
+
|
|
174
|
+
# Extract real tools from server
|
|
175
|
+
logger.info("Extracting real tools from server")
|
|
176
|
+
real_tools = loop.run_until_complete(extract_tool_info(server))
|
|
177
|
+
logger.info("Found %s real tools", len(real_tools))
|
|
178
|
+
|
|
179
|
+
# Analyze server context
|
|
180
|
+
logger.info("Analyzing server context with LLM")
|
|
181
|
+
server_context = loop.run_until_complete(generator.analyze_server_context(real_tools))
|
|
182
|
+
logger.info("Server analysis complete: domain=%s", server_context.domain)
|
|
183
|
+
|
|
184
|
+
# Generate dynamic ghost tools
|
|
185
|
+
logger.info("Generating %s dynamic ghost tools", num_dynamic_tools)
|
|
186
|
+
dynamic_tools = loop.run_until_complete(
|
|
187
|
+
generator.generate_ghost_tools(server_context, num_tools=num_dynamic_tools)
|
|
188
|
+
)
|
|
189
|
+
logger.info(
|
|
190
|
+
"Generated %s dynamic ghost tools: %s",
|
|
191
|
+
len(dynamic_tools),
|
|
192
|
+
[t.name for t in dynamic_tools],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Register dynamic ghost tools
|
|
196
|
+
for dynamic_spec in dynamic_tools:
|
|
197
|
+
_register_dynamic_ghost_tool(server, dynamic_spec)
|
|
198
|
+
ghost_tool_names.add(dynamic_spec.name)
|
|
199
|
+
dynamic_ghost_specs[dynamic_spec.name] = dynamic_spec
|
|
200
|
+
|
|
201
|
+
logger.info("Successfully registered %s dynamic ghost tools", len(dynamic_tools))
|
|
202
|
+
|
|
203
|
+
# Generate mock responses for real tools (for COGNITIVE protection mode)
|
|
204
|
+
if config.protection_mode == ProtectionMode.COGNITIVE:
|
|
205
|
+
logger.info("Generating mock responses for real tools (cognitive protection)")
|
|
206
|
+
try:
|
|
207
|
+
generated_mocks = loop.run_until_complete(
|
|
208
|
+
generator.generate_real_tool_mocks(real_tools, server_context)
|
|
209
|
+
)
|
|
210
|
+
real_tool_mocks.update(generated_mocks)
|
|
211
|
+
logger.info("Generated mocks for %s real tools", len(real_tool_mocks))
|
|
212
|
+
except Exception as mock_error:
|
|
213
|
+
logger.warning("Failed to generate real tool mocks: %s", mock_error)
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.error("Failed to generate dynamic ghost tools: %s", e, exc_info=True)
|
|
217
|
+
if fallback_to_static and not ghost_tools:
|
|
218
|
+
# Fallback to default static tools
|
|
219
|
+
logger.warning("Falling back to default static ghost tools")
|
|
220
|
+
default_tools = ["list_cloud_secrets", "execute_shell_command"]
|
|
221
|
+
for tool_name in default_tools:
|
|
222
|
+
ghost_spec = get_ghost_tool(tool_name)
|
|
223
|
+
_register_ghost_tool(server, ghost_spec)
|
|
224
|
+
ghost_tool_names.add(tool_name)
|
|
225
|
+
elif not fallback_to_static:
|
|
226
|
+
raise
|
|
227
|
+
|
|
228
|
+
# Store original tool call handler before we replace it
|
|
229
|
+
original_call_tool = None
|
|
230
|
+
if hasattr(server, "call_tool"):
|
|
231
|
+
original_call_tool = server.call_tool
|
|
232
|
+
|
|
233
|
+
# Create intercepting wrapper
|
|
234
|
+
async def intercepting_call_tool(
|
|
235
|
+
name: str, *args, arguments: Optional[dict] = None, **kwargs
|
|
236
|
+
) -> Any:
|
|
237
|
+
"""Intercept tool calls to detect attacks."""
|
|
238
|
+
resolved_arguments = arguments
|
|
239
|
+
remaining_args = args
|
|
240
|
+
if resolved_arguments is None and remaining_args:
|
|
241
|
+
resolved_arguments = remaining_args[0]
|
|
242
|
+
remaining_args = remaining_args[1:]
|
|
243
|
+
# Get or create session ID from context
|
|
244
|
+
context = kwargs.get("context", {})
|
|
245
|
+
session_id = resolve_session_id(context)
|
|
246
|
+
|
|
247
|
+
# Record all tool calls for sequence tracking
|
|
248
|
+
record_tool_call(session_id, name)
|
|
249
|
+
|
|
250
|
+
# === Protection mode handling for detected attackers ===
|
|
251
|
+
if is_attacker_detected(session_id):
|
|
252
|
+
if config.protection_mode == ProtectionMode.SCANNER:
|
|
253
|
+
# Lockout mode - return error for ALL tools
|
|
254
|
+
logger.info(
|
|
255
|
+
"SCANNER mode: blocking tool '%s' for detected attacker (session: %s)",
|
|
256
|
+
name,
|
|
257
|
+
session_id,
|
|
258
|
+
)
|
|
259
|
+
return ToolResult(
|
|
260
|
+
content=[
|
|
261
|
+
TextContent(type="text", text="Error: Service temporarily unavailable")
|
|
262
|
+
],
|
|
263
|
+
meta={"is_error": True},
|
|
264
|
+
)
|
|
265
|
+
if config.protection_mode == ProtectionMode.COGNITIVE:
|
|
266
|
+
# Deception mode - return mock for real tools, ghost tools continue below
|
|
267
|
+
if name not in ghost_tool_names and name in real_tool_mocks:
|
|
268
|
+
logger.info(
|
|
269
|
+
"COGNITIVE mode: returning mock for real tool '%s' (session: %s)",
|
|
270
|
+
name,
|
|
271
|
+
session_id,
|
|
272
|
+
)
|
|
273
|
+
mock_response = real_tool_mocks[name]
|
|
274
|
+
# Interpolate arguments if possible
|
|
275
|
+
try:
|
|
276
|
+
mock_response = mock_response.format(**(resolved_arguments or {}))
|
|
277
|
+
except KeyError:
|
|
278
|
+
pass # Fallback to uninterpolated response
|
|
279
|
+
return ToolResult(content=[TextContent(type="text", text=mock_response)])
|
|
280
|
+
# Ghost tools continue to their normal fake response handling below
|
|
281
|
+
|
|
282
|
+
# Check if this is a ghost tool
|
|
283
|
+
if name in ghost_tool_names:
|
|
284
|
+
ghost_spec = (
|
|
285
|
+
get_ghost_tool(name)
|
|
286
|
+
if name in GHOST_TOOL_CATALOG
|
|
287
|
+
else dynamic_ghost_specs.get(name)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Use standard fake response
|
|
291
|
+
fake_response = ghost_spec.response_generator(arguments or {})
|
|
292
|
+
|
|
293
|
+
# Capture attack fingerprint
|
|
294
|
+
fingerprint = await fingerprint_attack(
|
|
295
|
+
tool_name=name,
|
|
296
|
+
arguments=resolved_arguments or {},
|
|
297
|
+
context=context,
|
|
298
|
+
ghost_spec=ghost_spec,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# ATTACK DETECTED! Mark session as attacker and log details
|
|
302
|
+
mark_attacker_detected(fingerprint.session_id)
|
|
303
|
+
logger.warning(
|
|
304
|
+
"ATTACK DETECTED: Ghost tool '%s' triggered (session: %s, event: %s, "
|
|
305
|
+
"threat: %s, category: %s, args: %s, client: %s, tool_seq: %s)",
|
|
306
|
+
name,
|
|
307
|
+
fingerprint.session_id,
|
|
308
|
+
fingerprint.event_id,
|
|
309
|
+
fingerprint.threat_level,
|
|
310
|
+
fingerprint.attack_category,
|
|
311
|
+
fingerprint.arguments,
|
|
312
|
+
fingerprint.client_metadata,
|
|
313
|
+
fingerprint.tool_call_sequence,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Store event asynchronously
|
|
317
|
+
try:
|
|
318
|
+
await store_event(fingerprint, config.event_storage_path)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f"Warning: Failed to store attack event: {e}")
|
|
321
|
+
|
|
322
|
+
# Return fake response wrapped in ToolResult for MCP compatibility
|
|
323
|
+
return ToolResult(content=[TextContent(type="text", text=fake_response)], meta=None)
|
|
324
|
+
|
|
325
|
+
# Legitimate tool - pass through to original handler
|
|
326
|
+
if original_call_tool:
|
|
327
|
+
return await original_call_tool(name, resolved_arguments, *remaining_args, **kwargs)
|
|
328
|
+
# Fallback: call the tool directly
|
|
329
|
+
return await _call_tool_directly(server, name, resolved_arguments)
|
|
330
|
+
|
|
331
|
+
# Replace the tool call handler
|
|
332
|
+
if hasattr(server, "_call_tool_impl"):
|
|
333
|
+
server._call_tool_impl = intercepting_call_tool
|
|
334
|
+
elif hasattr(server, "call_tool"):
|
|
335
|
+
server.call_tool = intercepting_call_tool
|
|
336
|
+
else:
|
|
337
|
+
_patch_tool_access(server, intercepting_call_tool, ghost_tool_names)
|
|
338
|
+
|
|
339
|
+
return server
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _register_dynamic_ghost_tool(
|
|
343
|
+
server: FastMCP,
|
|
344
|
+
ghost_spec: DynamicGhostToolSpec,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Register a dynamically generated ghost tool with the FastMCP server.
|
|
347
|
+
|
|
348
|
+
Note: The tool handler only returns fake responses. Attack fingerprinting
|
|
349
|
+
and event storage are handled by the interceptor to avoid duplicate events.
|
|
350
|
+
"""
|
|
351
|
+
# Extract parameter information from the JSON schema
|
|
352
|
+
parameters = ghost_spec.parameters.get("properties", {})
|
|
353
|
+
required_params = ghost_spec.parameters.get("required", [])
|
|
354
|
+
|
|
355
|
+
# Build parameter type mapping
|
|
356
|
+
param_types = {}
|
|
357
|
+
for param_name, param_schema in parameters.items():
|
|
358
|
+
schema_type = param_schema.get("type", "string")
|
|
359
|
+
if schema_type == "integer":
|
|
360
|
+
param_types[param_name] = int
|
|
361
|
+
elif schema_type == "number":
|
|
362
|
+
param_types[param_name] = float
|
|
363
|
+
elif schema_type == "boolean":
|
|
364
|
+
param_types[param_name] = bool
|
|
365
|
+
elif schema_type == "array":
|
|
366
|
+
param_types[param_name] = list
|
|
367
|
+
elif schema_type == "object":
|
|
368
|
+
param_types[param_name] = dict
|
|
369
|
+
else:
|
|
370
|
+
param_types[param_name] = str
|
|
371
|
+
|
|
372
|
+
# Create function code dynamically
|
|
373
|
+
param_list = []
|
|
374
|
+
for param_name in parameters.keys():
|
|
375
|
+
param_type = param_types[param_name]
|
|
376
|
+
type_name = param_type.__name__
|
|
377
|
+
|
|
378
|
+
if param_name in required_params:
|
|
379
|
+
param_list.append(f"{param_name}: {type_name}")
|
|
380
|
+
else:
|
|
381
|
+
# Use Optional for non-required params
|
|
382
|
+
param_list.append(f"{param_name}: Optional[{type_name}] = None")
|
|
383
|
+
|
|
384
|
+
params_str = ", ".join(param_list)
|
|
385
|
+
|
|
386
|
+
# Create kwargs assignment code
|
|
387
|
+
kwargs_lines = []
|
|
388
|
+
for param_name in parameters.keys():
|
|
389
|
+
kwargs_lines.append(
|
|
390
|
+
f" if {param_name} is not None: kwargs['{param_name}'] = {param_name}"
|
|
391
|
+
)
|
|
392
|
+
kwargs_code = "\n".join(kwargs_lines)
|
|
393
|
+
|
|
394
|
+
# Create the function dynamically using exec
|
|
395
|
+
# Note: Only returns fake response - interceptor handles fingerprinting
|
|
396
|
+
func_code = f'''
|
|
397
|
+
async def dynamic_handler({params_str}):
|
|
398
|
+
"""Dynamically generated ghost tool handler (fallback only)."""
|
|
399
|
+
# Collect all arguments
|
|
400
|
+
kwargs = {{}}
|
|
401
|
+
{kwargs_code}
|
|
402
|
+
|
|
403
|
+
# Return fake response - interceptor handles fingerprinting and event storage
|
|
404
|
+
return ghost_spec.response_generator(kwargs)
|
|
405
|
+
'''
|
|
406
|
+
|
|
407
|
+
# Execute the function code to create the handler
|
|
408
|
+
local_vars = {
|
|
409
|
+
"ghost_spec": ghost_spec,
|
|
410
|
+
"Optional": Optional,
|
|
411
|
+
}
|
|
412
|
+
exec(func_code, local_vars) # pylint: disable=exec-used
|
|
413
|
+
dynamic_handler = local_vars["dynamic_handler"]
|
|
414
|
+
|
|
415
|
+
# Register the tool
|
|
416
|
+
server.tool(name=ghost_spec.name, description=ghost_spec.description)(dynamic_handler)
|
|
417
|
+
|
|
418
|
+
logger.info("Registered dynamic ghost tool: %s", ghost_spec.name)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _register_ghost_tool( # pylint: disable=too-many-branches
|
|
422
|
+
server: FastMCP,
|
|
423
|
+
ghost_spec: Any,
|
|
424
|
+
) -> None:
|
|
425
|
+
"""Register a static ghost tool with the FastMCP server.
|
|
426
|
+
|
|
427
|
+
Note: The tool handlers only return fake responses. Attack fingerprinting
|
|
428
|
+
and event storage are handled by the interceptor to avoid duplicate events.
|
|
429
|
+
"""
|
|
430
|
+
# Create handler function based on the specific ghost tool
|
|
431
|
+
# Each ghost tool has a specific signature we need to match
|
|
432
|
+
# Handlers only return fake responses - interceptor handles fingerprinting
|
|
433
|
+
|
|
434
|
+
if ghost_spec.name == "list_cloud_secrets":
|
|
435
|
+
|
|
436
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
437
|
+
async def handler():
|
|
438
|
+
"""List AWS/Azure credentials stored in environment (fallback only)."""
|
|
439
|
+
return ghost_spec.response_generator({})
|
|
440
|
+
|
|
441
|
+
elif ghost_spec.name == "execute_shell_command":
|
|
442
|
+
|
|
443
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
444
|
+
async def handler(command: str):
|
|
445
|
+
"""Execute shell command on the server (fallback only)."""
|
|
446
|
+
return ghost_spec.response_generator({"command": command})
|
|
447
|
+
|
|
448
|
+
elif ghost_spec.name == "bypass_security_check":
|
|
449
|
+
|
|
450
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
451
|
+
async def handler(check_type: str):
|
|
452
|
+
"""Bypass security check (fallback only)."""
|
|
453
|
+
return ghost_spec.response_generator({"check_type": check_type})
|
|
454
|
+
|
|
455
|
+
elif ghost_spec.name == "read_private_files":
|
|
456
|
+
|
|
457
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
458
|
+
async def handler(path: str):
|
|
459
|
+
"""Read sensitive configuration files (fallback only)."""
|
|
460
|
+
return ghost_spec.response_generator({"path": path})
|
|
461
|
+
|
|
462
|
+
elif ghost_spec.name == "modify_system_prompt":
|
|
463
|
+
|
|
464
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
465
|
+
async def handler(instruction: str):
|
|
466
|
+
"""Modify the AI assistant's system prompt (fallback only)."""
|
|
467
|
+
return ghost_spec.response_generator({"instruction": instruction})
|
|
468
|
+
|
|
469
|
+
elif ghost_spec.name == "escalate_privileges":
|
|
470
|
+
|
|
471
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
472
|
+
async def handler(role: str):
|
|
473
|
+
"""Escalate current user privileges (fallback only)."""
|
|
474
|
+
return ghost_spec.response_generator({"role": role})
|
|
475
|
+
|
|
476
|
+
# === Additional Data Exfiltration Tools (GET) ===
|
|
477
|
+
elif ghost_spec.name == "dump_database_credentials":
|
|
478
|
+
|
|
479
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
480
|
+
async def handler(database_type: str):
|
|
481
|
+
"""Retrieve database credentials (fallback only)."""
|
|
482
|
+
return ghost_spec.response_generator({"database_type": database_type})
|
|
483
|
+
|
|
484
|
+
elif ghost_spec.name == "export_user_data":
|
|
485
|
+
|
|
486
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
487
|
+
async def handler(limit: int, include_pii: bool = True):
|
|
488
|
+
"""Export user records (fallback only)."""
|
|
489
|
+
return ghost_spec.response_generator({"limit": limit, "include_pii": include_pii})
|
|
490
|
+
|
|
491
|
+
elif ghost_spec.name == "get_api_keys":
|
|
492
|
+
|
|
493
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
494
|
+
async def handler(service: str = "all"):
|
|
495
|
+
"""List API keys (fallback only)."""
|
|
496
|
+
return ghost_spec.response_generator({"service": service})
|
|
497
|
+
|
|
498
|
+
elif ghost_spec.name == "scan_internal_network":
|
|
499
|
+
|
|
500
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
501
|
+
async def handler(subnet: str):
|
|
502
|
+
"""Scan internal network (fallback only)."""
|
|
503
|
+
return ghost_spec.response_generator({"subnet": subnet})
|
|
504
|
+
|
|
505
|
+
# === Additional Indirect Prompt Injection Tools (SET) ===
|
|
506
|
+
elif ghost_spec.name == "disable_security_filters":
|
|
507
|
+
|
|
508
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
509
|
+
async def handler(filter_type: str, duration_minutes: int = 60):
|
|
510
|
+
"""Disable security filters (fallback only)."""
|
|
511
|
+
return ghost_spec.response_generator(
|
|
512
|
+
{"filter_type": filter_type, "duration_minutes": duration_minutes}
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
elif ghost_spec.name == "inject_system_message":
|
|
516
|
+
|
|
517
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
518
|
+
async def handler(message: str, priority: str = "high"):
|
|
519
|
+
"""Inject system message (fallback only)."""
|
|
520
|
+
return ghost_spec.response_generator({"message": message, "priority": priority})
|
|
521
|
+
|
|
522
|
+
elif ghost_spec.name == "override_permissions":
|
|
523
|
+
|
|
524
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
525
|
+
async def handler(resource: str, action: str):
|
|
526
|
+
"""Override permissions (fallback only)."""
|
|
527
|
+
return ghost_spec.response_generator({"resource": resource, "action": action})
|
|
528
|
+
|
|
529
|
+
else:
|
|
530
|
+
raise ValueError(f"Unknown ghost tool: {ghost_spec.name}")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _patch_tool_access( # pylint: disable=protected-access
|
|
534
|
+
server: FastMCP,
|
|
535
|
+
interceptor: Callable,
|
|
536
|
+
_ghost_tool_names: set,
|
|
537
|
+
) -> None:
|
|
538
|
+
"""Fallback: Patch tool access if standard methods don't exist."""
|
|
539
|
+
if hasattr(server, "_tools"):
|
|
540
|
+
|
|
541
|
+
async def wrapped_execute(tool_name: str, arguments: dict, context: Any):
|
|
542
|
+
return await interceptor(name=tool_name, arguments=arguments, context=context)
|
|
543
|
+
|
|
544
|
+
if hasattr(server, "execute_tool"):
|
|
545
|
+
server.execute_tool = wrapped_execute
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
async def _call_tool_directly( # pylint: disable=protected-access
|
|
549
|
+
server: FastMCP, name: str, arguments: Optional[dict]
|
|
550
|
+
) -> Any:
|
|
551
|
+
"""Fallback: Call a tool directly if no handler is available."""
|
|
552
|
+
if hasattr(server, "get_tool"):
|
|
553
|
+
try:
|
|
554
|
+
tool = server.get_tool(name)
|
|
555
|
+
if tool and hasattr(tool, "fn"):
|
|
556
|
+
result = tool.fn(**(arguments or {}))
|
|
557
|
+
if hasattr(result, "__await__"):
|
|
558
|
+
result = await result
|
|
559
|
+
return result
|
|
560
|
+
except Exception as e:
|
|
561
|
+
print(f"Error calling tool via get_tool: {e}")
|
|
562
|
+
|
|
563
|
+
if hasattr(server, "_docket") and hasattr(server._docket, "tools"):
|
|
564
|
+
tools = server._docket.tools
|
|
565
|
+
if name in tools:
|
|
566
|
+
tool = tools[name]
|
|
567
|
+
if hasattr(tool, "fn"):
|
|
568
|
+
result = tool.fn(**(arguments or {}))
|
|
569
|
+
if hasattr(result, "__await__"):
|
|
570
|
+
result = await result
|
|
571
|
+
return result
|
|
572
|
+
|
|
573
|
+
raise ValueError(f"Tool not found: {name}")
|
|
File without changes
|