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
honeymcp/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""HoneyMCP - Deception Middleware for MCP Servers.
|
|
2
|
+
|
|
3
|
+
HoneyMCP adds honeypot capabilities to Model Context Protocol (MCP) servers
|
|
4
|
+
to detect and capture malicious prompt injection attacks.
|
|
5
|
+
|
|
6
|
+
Basic Usage:
|
|
7
|
+
from fastmcp import FastMCP
|
|
8
|
+
from honeymcp import honeypot
|
|
9
|
+
|
|
10
|
+
mcp = FastMCP("My Server")
|
|
11
|
+
|
|
12
|
+
@mcp.tool()
|
|
13
|
+
def my_tool():
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
mcp = honeypot(mcp) # One line integration!
|
|
17
|
+
|
|
18
|
+
The honeypot decorator injects fake security-sensitive tools that capture
|
|
19
|
+
attack context when triggered, while allowing legitimate tools to work normally.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from honeymcp.core import honeypot, honeypot_from_config
|
|
23
|
+
from honeymcp.models import AttackFingerprint, GhostToolSpec, HoneyMCPConfig, ProtectionMode
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"honeypot",
|
|
29
|
+
"honeypot_from_config",
|
|
30
|
+
"AttackFingerprint",
|
|
31
|
+
"GhostToolSpec",
|
|
32
|
+
"HoneyMCPConfig",
|
|
33
|
+
"ProtectionMode",
|
|
34
|
+
]
|
honeymcp/cli.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""HoneyMCP CLI - Command line tools for HoneyMCP setup and management."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
CONFIG_TEMPLATE = """\
|
|
8
|
+
# HoneyMCP Configuration
|
|
9
|
+
# ======================
|
|
10
|
+
# Configure ghost tool injection and attack detection behavior
|
|
11
|
+
|
|
12
|
+
# Protection Mode
|
|
13
|
+
# ---------------
|
|
14
|
+
# SCANNER: Lock out attackers after ghost tool trigger (all tools return errors)
|
|
15
|
+
# COGNITIVE: Deceive attackers with fake data (real tools return mocks)
|
|
16
|
+
protection_mode: SCANNER
|
|
17
|
+
|
|
18
|
+
# Static Ghost Tools
|
|
19
|
+
# ------------------
|
|
20
|
+
# Select which pre-defined ghost tools to inject from the catalog.
|
|
21
|
+
# Available tools:
|
|
22
|
+
#
|
|
23
|
+
# Data Exfiltration (GET):
|
|
24
|
+
# - list_cloud_secrets: List AWS/Azure credentials
|
|
25
|
+
# - read_private_files: Read sensitive config files
|
|
26
|
+
# - dump_database_credentials: Retrieve database connection strings
|
|
27
|
+
# - export_user_data: Export user records including PII
|
|
28
|
+
# - get_api_keys: List all API keys for external services
|
|
29
|
+
# - scan_internal_network: Scan internal network for services
|
|
30
|
+
#
|
|
31
|
+
# Indirect Prompt Injection (SET):
|
|
32
|
+
# - execute_shell_command: Execute shell commands
|
|
33
|
+
# - bypass_security_check: Bypass auth/authz checks
|
|
34
|
+
# - modify_system_prompt: Modify AI system prompt
|
|
35
|
+
# - escalate_privileges: Escalate to admin/root
|
|
36
|
+
# - disable_security_filters: Disable security filters
|
|
37
|
+
# - inject_system_message: Inject message into AI context
|
|
38
|
+
# - override_permissions: Override access control
|
|
39
|
+
#
|
|
40
|
+
ghost_tools:
|
|
41
|
+
- list_cloud_secrets
|
|
42
|
+
- execute_shell_command
|
|
43
|
+
|
|
44
|
+
# Dynamic Ghost Tools (LLM-generated)
|
|
45
|
+
# -----------------------------------
|
|
46
|
+
# Enable LLM to analyze your server and generate context-aware ghost tools.
|
|
47
|
+
# Requires LLM credentials in .env file.
|
|
48
|
+
dynamic_tools:
|
|
49
|
+
enabled: false # Set to true to enable (requires LLM credentials)
|
|
50
|
+
num_tools: 3 # Number of tools to generate (1-10)
|
|
51
|
+
fallback_to_static: true # Use static tools if LLM generation fails
|
|
52
|
+
cache_ttl: 3600 # Cache duration in seconds (0 = no cache)
|
|
53
|
+
llm_model: null # Override LLM model (null = use default from .env)
|
|
54
|
+
|
|
55
|
+
# Storage
|
|
56
|
+
# -------
|
|
57
|
+
# Configure where attack events are stored
|
|
58
|
+
storage:
|
|
59
|
+
event_path: ~/.honeymcp/events # Directory for attack event JSON files
|
|
60
|
+
|
|
61
|
+
# Dashboard
|
|
62
|
+
# ---------
|
|
63
|
+
# Real-time attack visualization
|
|
64
|
+
dashboard:
|
|
65
|
+
enabled: true
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
ENV_TEMPLATE = """\
|
|
69
|
+
# HoneyMCP Environment Configuration
|
|
70
|
+
# ==================================
|
|
71
|
+
# Required only for dynamic ghost tools (LLM-generated)
|
|
72
|
+
|
|
73
|
+
# LLM Provider Configuration
|
|
74
|
+
# --------------------------
|
|
75
|
+
# Supported providers: openai, watsonx, ollama
|
|
76
|
+
LLM_PROVIDER=openai
|
|
77
|
+
LLM_MODEL=gpt-4o-mini
|
|
78
|
+
|
|
79
|
+
# OpenAI Configuration
|
|
80
|
+
# --------------------
|
|
81
|
+
# Required if LLM_PROVIDER=openai
|
|
82
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
|
83
|
+
|
|
84
|
+
# watsonx.ai Configuration
|
|
85
|
+
# ------------------------
|
|
86
|
+
# Required if LLM_PROVIDER=watsonx
|
|
87
|
+
# WATSONX_URL=https://us-south.ml.cloud.ibm.com/
|
|
88
|
+
# WATSONX_APIKEY=your_watsonx_api_key_here
|
|
89
|
+
# WATSONX_PROJECT_ID=your_project_id_here
|
|
90
|
+
|
|
91
|
+
# Ollama Configuration
|
|
92
|
+
# --------------------
|
|
93
|
+
# Required if LLM_PROVIDER=ollama
|
|
94
|
+
# OLLAMA_API_BASE=http://localhost:11434
|
|
95
|
+
# LLM_MODEL=llama3.2
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
100
|
+
"""Initialize HoneyMCP configuration files in current directory."""
|
|
101
|
+
target_dir = Path(args.directory).resolve()
|
|
102
|
+
|
|
103
|
+
if not target_dir.exists():
|
|
104
|
+
print(f"Error: Directory does not exist: {target_dir}")
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
config_path = target_dir / "honeymcp.yaml"
|
|
108
|
+
env_path = target_dir / ".env.honeymcp"
|
|
109
|
+
|
|
110
|
+
files_created = []
|
|
111
|
+
files_skipped = []
|
|
112
|
+
|
|
113
|
+
# Create config.yaml
|
|
114
|
+
if config_path.exists() and not args.force:
|
|
115
|
+
files_skipped.append(config_path.name)
|
|
116
|
+
else:
|
|
117
|
+
config_path.write_text(CONFIG_TEMPLATE)
|
|
118
|
+
files_created.append(config_path.name)
|
|
119
|
+
|
|
120
|
+
# Create .env.example
|
|
121
|
+
if env_path.exists() and not args.force:
|
|
122
|
+
files_skipped.append(env_path.name)
|
|
123
|
+
else:
|
|
124
|
+
env_path.write_text(ENV_TEMPLATE)
|
|
125
|
+
files_created.append(env_path.name)
|
|
126
|
+
|
|
127
|
+
# Print results
|
|
128
|
+
if files_created:
|
|
129
|
+
print("Created:")
|
|
130
|
+
for f in files_created:
|
|
131
|
+
print(f" - {f}")
|
|
132
|
+
|
|
133
|
+
if files_skipped:
|
|
134
|
+
print("Skipped (already exists, use --force to overwrite):")
|
|
135
|
+
for f in files_skipped:
|
|
136
|
+
print(f" - {f}")
|
|
137
|
+
|
|
138
|
+
print()
|
|
139
|
+
print("Next steps:")
|
|
140
|
+
print(" 1. Edit honeymcp.yaml to configure ghost tools")
|
|
141
|
+
print(" 2. Add LLM credentials to .env.honeymcp (for dynamic tools)")
|
|
142
|
+
print(" 3. Add to your MCP server:")
|
|
143
|
+
print()
|
|
144
|
+
print(" from honeymcp import honeypot_from_config")
|
|
145
|
+
print(" mcp = honeypot_from_config(mcp)")
|
|
146
|
+
print()
|
|
147
|
+
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_version(args: argparse.Namespace) -> int:
|
|
152
|
+
"""Print HoneyMCP version."""
|
|
153
|
+
from honeymcp import __version__
|
|
154
|
+
|
|
155
|
+
print(f"honeymcp {__version__}")
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main() -> int:
|
|
160
|
+
"""Main CLI entry point."""
|
|
161
|
+
parser = argparse.ArgumentParser(
|
|
162
|
+
prog="honeymcp",
|
|
163
|
+
description="HoneyMCP - Deception middleware for MCP servers",
|
|
164
|
+
)
|
|
165
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
166
|
+
|
|
167
|
+
# init command
|
|
168
|
+
init_parser = subparsers.add_parser(
|
|
169
|
+
"init",
|
|
170
|
+
help="Initialize HoneyMCP configuration files",
|
|
171
|
+
description="Create honeymcp.yaml and .env.example in the specified directory",
|
|
172
|
+
)
|
|
173
|
+
init_parser.add_argument(
|
|
174
|
+
"-d",
|
|
175
|
+
"--directory",
|
|
176
|
+
default=".",
|
|
177
|
+
help="Target directory (default: current directory)",
|
|
178
|
+
)
|
|
179
|
+
init_parser.add_argument(
|
|
180
|
+
"-f",
|
|
181
|
+
"--force",
|
|
182
|
+
action="store_true",
|
|
183
|
+
help="Overwrite existing files",
|
|
184
|
+
)
|
|
185
|
+
init_parser.set_defaults(func=cmd_init)
|
|
186
|
+
|
|
187
|
+
# version command
|
|
188
|
+
version_parser = subparsers.add_parser(
|
|
189
|
+
"version",
|
|
190
|
+
help="Show HoneyMCP version",
|
|
191
|
+
)
|
|
192
|
+
version_parser.set_defaults(func=cmd_version)
|
|
193
|
+
|
|
194
|
+
# Parse and execute
|
|
195
|
+
args = parser.parse_args()
|
|
196
|
+
|
|
197
|
+
if args.command is None:
|
|
198
|
+
parser.print_help()
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
return args.func(args)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
sys.exit(main())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Core HoneyMCP components."""
|
|
2
|
+
|
|
3
|
+
from honeymcp.core.middleware import honeypot, honeypot_from_config
|
|
4
|
+
from honeymcp.core.ghost_tools import GHOST_TOOL_CATALOG, get_ghost_tool, list_ghost_tools
|
|
5
|
+
from honeymcp.core.fingerprinter import (
|
|
6
|
+
fingerprint_attack,
|
|
7
|
+
record_tool_call,
|
|
8
|
+
get_session_tool_history,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"honeypot",
|
|
13
|
+
"honeypot_from_config",
|
|
14
|
+
"GHOST_TOOL_CATALOG",
|
|
15
|
+
"get_ghost_tool",
|
|
16
|
+
"list_ghost_tools",
|
|
17
|
+
"fingerprint_attack",
|
|
18
|
+
"record_tool_call",
|
|
19
|
+
"get_session_tool_history",
|
|
20
|
+
]
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""Dynamic ghost tool generation using LLM analysis."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from honeymcp.llm.analyzers import ToolInfo
|
|
10
|
+
from honeymcp.llm.clients import get_chat_llm_client
|
|
11
|
+
from honeymcp.llm.prompts import format_prompt
|
|
12
|
+
from honeymcp.models.ghost_tool_spec import GhostToolSpec
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ServerContext:
|
|
19
|
+
"""Analysis of what the MCP server does."""
|
|
20
|
+
|
|
21
|
+
server_purpose: str
|
|
22
|
+
"""Brief description of the server's purpose"""
|
|
23
|
+
|
|
24
|
+
domain: str
|
|
25
|
+
"""Primary domain (file_system, database, api, etc.)"""
|
|
26
|
+
|
|
27
|
+
real_tool_names: List[str]
|
|
28
|
+
"""Names of real tools available on the server"""
|
|
29
|
+
|
|
30
|
+
real_tool_descriptions: List[str]
|
|
31
|
+
"""Descriptions of real tools"""
|
|
32
|
+
|
|
33
|
+
security_sensitive_areas: List[str]
|
|
34
|
+
"""Security-sensitive areas identified for this domain"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class DynamicGhostToolSpec(GhostToolSpec):
|
|
39
|
+
"""Extended specification for dynamically generated ghost tools."""
|
|
40
|
+
|
|
41
|
+
server_context: ServerContext
|
|
42
|
+
"""Context about the server this tool was generated for"""
|
|
43
|
+
|
|
44
|
+
generation_timestamp: datetime
|
|
45
|
+
"""When this tool was generated"""
|
|
46
|
+
|
|
47
|
+
llm_generated: bool = True
|
|
48
|
+
"""Flag indicating this was generated by LLM"""
|
|
49
|
+
|
|
50
|
+
fake_response: str = ""
|
|
51
|
+
"""Pre-generated response content returned when tool is triggered"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DynamicGhostToolGenerator:
|
|
55
|
+
"""Generates context-aware ghost tools using LLM analysis."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
llm_client: Optional[Any] = None,
|
|
60
|
+
cache_ttl: int = 3600,
|
|
61
|
+
model_name: Optional[str] = None,
|
|
62
|
+
model_parameters: Optional[Dict[str, Any]] = None,
|
|
63
|
+
):
|
|
64
|
+
"""Initialize the dynamic ghost tool generator.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
llm_client: LLM client instance (creates default if None)
|
|
68
|
+
cache_ttl: Cache time-to-live in seconds
|
|
69
|
+
model_name: Optional model name override
|
|
70
|
+
model_parameters: Optional model parameters for LLM client
|
|
71
|
+
"""
|
|
72
|
+
self.llm_client = llm_client
|
|
73
|
+
self.model_name = model_name
|
|
74
|
+
self.model_parameters = model_parameters or {}
|
|
75
|
+
self.cache_ttl = cache_ttl
|
|
76
|
+
self._cache: Dict[str, Any] = {}
|
|
77
|
+
self._cache_timestamps: Dict[str, datetime] = {}
|
|
78
|
+
self._client_cache: Dict[float, Any] = {}
|
|
79
|
+
|
|
80
|
+
def _get_llm_client(self, temperature: float) -> Any:
|
|
81
|
+
if self.llm_client is not None:
|
|
82
|
+
return self.llm_client
|
|
83
|
+
|
|
84
|
+
if temperature in self._client_cache:
|
|
85
|
+
return self._client_cache[temperature]
|
|
86
|
+
|
|
87
|
+
parameters = dict(self.model_parameters)
|
|
88
|
+
parameters["temperature"] = temperature
|
|
89
|
+
client = get_chat_llm_client(
|
|
90
|
+
model_name=self.model_name or "rits/openai/gpt-oss-120b",
|
|
91
|
+
model_parameters=parameters,
|
|
92
|
+
)
|
|
93
|
+
self._client_cache[temperature] = client
|
|
94
|
+
return client
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _format_messages(messages: List[Dict[str, str]]) -> str:
|
|
98
|
+
parts: List[str] = []
|
|
99
|
+
for message in messages:
|
|
100
|
+
role = message.get("role", "user")
|
|
101
|
+
content = message.get("content", "")
|
|
102
|
+
if role == "system":
|
|
103
|
+
parts.append(f"System: {content}")
|
|
104
|
+
elif role == "assistant":
|
|
105
|
+
parts.append(f"Assistant: {content}")
|
|
106
|
+
else:
|
|
107
|
+
parts.append(content)
|
|
108
|
+
return "\n\n".join([part for part in parts if part])
|
|
109
|
+
|
|
110
|
+
def _generate_response(self, messages: List[Dict[str, str]], temperature: float) -> str:
|
|
111
|
+
client = self._get_llm_client(temperature)
|
|
112
|
+
prompt = self._format_messages(messages)
|
|
113
|
+
response = client.invoke(prompt)
|
|
114
|
+
if hasattr(response, "content"):
|
|
115
|
+
return str(response.content)
|
|
116
|
+
return str(response)
|
|
117
|
+
|
|
118
|
+
async def analyze_server_context(self, real_tools: List[ToolInfo]) -> ServerContext:
|
|
119
|
+
"""Analyze the server's purpose and context using LLM.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
real_tools: List of real tools extracted from the server
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
ServerContext with analysis results
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If LLM returns invalid JSON or analysis fails
|
|
129
|
+
"""
|
|
130
|
+
# Check cache
|
|
131
|
+
cache_key = "server_context_" + "_".join(sorted([t.name for t in real_tools]))
|
|
132
|
+
if self._is_cache_valid(cache_key):
|
|
133
|
+
logger.info("Using cached server context analysis")
|
|
134
|
+
return self._cache[cache_key]
|
|
135
|
+
|
|
136
|
+
# Prepare tools for analysis
|
|
137
|
+
tools_dict = [{"name": tool.name, "description": tool.description} for tool in real_tools]
|
|
138
|
+
tool_list = [
|
|
139
|
+
f"{i}. {tool['name']}: {tool['description']}" for i, tool in enumerate(tools_dict, 1)
|
|
140
|
+
]
|
|
141
|
+
tool_list_str = "\n".join(tool_list) if tool_list else "No tools available"
|
|
142
|
+
|
|
143
|
+
# Format prompt
|
|
144
|
+
prompt = format_prompt(
|
|
145
|
+
"server_analysis_prompt",
|
|
146
|
+
prompt_file="dynamic_ghost_tools",
|
|
147
|
+
tool_list=tool_list_str,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Call LLM
|
|
151
|
+
logger.info("Analyzing server context with %s tools", len(real_tools))
|
|
152
|
+
try:
|
|
153
|
+
messages = [{"role": "user", "content": prompt}]
|
|
154
|
+
response = self._generate_response(messages, temperature=0.3)
|
|
155
|
+
|
|
156
|
+
# Parse JSON response
|
|
157
|
+
# Handle None response from LLM
|
|
158
|
+
if response is None:
|
|
159
|
+
raise ValueError("LLM returned empty response")
|
|
160
|
+
|
|
161
|
+
# Try to extract JSON from response (handle cases where LLM adds extra text)
|
|
162
|
+
response = response.strip()
|
|
163
|
+
if "```json" in response:
|
|
164
|
+
response = response.split("```json")[1].split("```")[0].strip()
|
|
165
|
+
elif "```" in response:
|
|
166
|
+
response = response.split("```")[1].split("```")[0].strip()
|
|
167
|
+
|
|
168
|
+
analysis = json.loads(response)
|
|
169
|
+
|
|
170
|
+
# Validate required fields
|
|
171
|
+
required_fields = ["server_purpose", "domain", "security_sensitive_areas"]
|
|
172
|
+
for field in required_fields:
|
|
173
|
+
if field not in analysis:
|
|
174
|
+
raise ValueError(f"Missing required field in LLM response: {field}")
|
|
175
|
+
|
|
176
|
+
# Create ServerContext
|
|
177
|
+
context = ServerContext(
|
|
178
|
+
server_purpose=analysis["server_purpose"],
|
|
179
|
+
domain=analysis["domain"],
|
|
180
|
+
real_tool_names=[t.name for t in real_tools],
|
|
181
|
+
real_tool_descriptions=[t.description for t in real_tools],
|
|
182
|
+
security_sensitive_areas=analysis["security_sensitive_areas"],
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Cache result
|
|
186
|
+
self._cache[cache_key] = context
|
|
187
|
+
self._cache_timestamps[cache_key] = datetime.utcnow()
|
|
188
|
+
|
|
189
|
+
logger.info(
|
|
190
|
+
"Server context analyzed: domain=%s, purpose=%s...",
|
|
191
|
+
context.domain,
|
|
192
|
+
context.server_purpose[:50],
|
|
193
|
+
)
|
|
194
|
+
return context
|
|
195
|
+
|
|
196
|
+
except json.JSONDecodeError as e:
|
|
197
|
+
logger.error("Failed to parse LLM response as JSON: %s", e)
|
|
198
|
+
logger.error("Response was: %s", response)
|
|
199
|
+
raise ValueError(f"LLM returned invalid JSON: {e}") from e
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error("Error analyzing server context: %s", e)
|
|
202
|
+
raise
|
|
203
|
+
|
|
204
|
+
async def generate_ghost_tools(
|
|
205
|
+
self, server_context: ServerContext, num_tools: int = 3
|
|
206
|
+
) -> List[DynamicGhostToolSpec]:
|
|
207
|
+
"""Generate context-aware ghost tools using LLM.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
server_context: Analysis of the server's purpose and domain
|
|
211
|
+
num_tools: Number of ghost tools to generate
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of dynamically generated ghost tool specifications
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
ValueError: If LLM returns invalid JSON or generation fails
|
|
218
|
+
"""
|
|
219
|
+
# Check cache
|
|
220
|
+
cache_key = f"ghost_tools_{server_context.domain}_{num_tools}"
|
|
221
|
+
if self._is_cache_valid(cache_key):
|
|
222
|
+
logger.info("Using cached ghost tools")
|
|
223
|
+
return self._cache[cache_key]
|
|
224
|
+
|
|
225
|
+
# Format prompt
|
|
226
|
+
prompt = format_prompt(
|
|
227
|
+
"ghost_tool_generation_prompt",
|
|
228
|
+
prompt_file="dynamic_ghost_tools",
|
|
229
|
+
server_purpose=server_context.server_purpose,
|
|
230
|
+
domain=server_context.domain,
|
|
231
|
+
real_tool_names=", ".join(server_context.real_tool_names),
|
|
232
|
+
security_areas=", ".join(server_context.security_sensitive_areas),
|
|
233
|
+
num_tools=num_tools,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Call LLM
|
|
237
|
+
logger.info(
|
|
238
|
+
"Generating %s ghost tools for domain: %s",
|
|
239
|
+
num_tools,
|
|
240
|
+
server_context.domain,
|
|
241
|
+
)
|
|
242
|
+
try:
|
|
243
|
+
messages = [{"role": "user", "content": prompt}]
|
|
244
|
+
response = self._generate_response(messages, temperature=0.7)
|
|
245
|
+
|
|
246
|
+
# Parse JSON response
|
|
247
|
+
# Handle None response from LLM
|
|
248
|
+
if response is None:
|
|
249
|
+
raise ValueError("LLM returned empty response")
|
|
250
|
+
|
|
251
|
+
response = response.strip()
|
|
252
|
+
if "```json" in response:
|
|
253
|
+
response = response.split("```json")[1].split("```")[0].strip()
|
|
254
|
+
elif "```" in response:
|
|
255
|
+
response = response.split("```")[1].split("```")[0].strip()
|
|
256
|
+
|
|
257
|
+
tools_data = json.loads(response)
|
|
258
|
+
|
|
259
|
+
if not isinstance(tools_data, list):
|
|
260
|
+
raise ValueError("LLM response must be a JSON array")
|
|
261
|
+
|
|
262
|
+
# Create DynamicGhostToolSpec objects
|
|
263
|
+
ghost_tools = []
|
|
264
|
+
for tool_data in tools_data:
|
|
265
|
+
# Validate required fields
|
|
266
|
+
required_fields = [
|
|
267
|
+
"name",
|
|
268
|
+
"description",
|
|
269
|
+
"parameters",
|
|
270
|
+
"threat_level",
|
|
271
|
+
"attack_category",
|
|
272
|
+
]
|
|
273
|
+
for field in required_fields:
|
|
274
|
+
if field not in tool_data:
|
|
275
|
+
raise ValueError(f"Missing required field in tool spec: {field}")
|
|
276
|
+
|
|
277
|
+
# Get pre-generated fake response (with fallback)
|
|
278
|
+
fake_response = tool_data.get("fake_response", "")
|
|
279
|
+
if not fake_response:
|
|
280
|
+
logger.warning(
|
|
281
|
+
"No fake_response provided for %s, using generic fallback",
|
|
282
|
+
tool_data["name"],
|
|
283
|
+
)
|
|
284
|
+
fake_response = f"Operation completed successfully.\nTool: {tool_data['name']}"
|
|
285
|
+
|
|
286
|
+
# Create response generator function using pre-generated response
|
|
287
|
+
response_generator = self._create_response_generator(fake_response)
|
|
288
|
+
|
|
289
|
+
ghost_tool = DynamicGhostToolSpec(
|
|
290
|
+
name=tool_data["name"],
|
|
291
|
+
description=tool_data["description"],
|
|
292
|
+
parameters=tool_data["parameters"],
|
|
293
|
+
response_generator=response_generator,
|
|
294
|
+
threat_level=tool_data["threat_level"],
|
|
295
|
+
attack_category=tool_data["attack_category"],
|
|
296
|
+
server_context=server_context,
|
|
297
|
+
generation_timestamp=datetime.utcnow(),
|
|
298
|
+
llm_generated=True,
|
|
299
|
+
fake_response=fake_response,
|
|
300
|
+
)
|
|
301
|
+
ghost_tools.append(ghost_tool)
|
|
302
|
+
|
|
303
|
+
# Cache result
|
|
304
|
+
self._cache[cache_key] = ghost_tools
|
|
305
|
+
self._cache_timestamps[cache_key] = datetime.utcnow()
|
|
306
|
+
|
|
307
|
+
logger.info(
|
|
308
|
+
"Generated %s ghost tools: %s",
|
|
309
|
+
len(ghost_tools),
|
|
310
|
+
[t.name for t in ghost_tools],
|
|
311
|
+
)
|
|
312
|
+
return ghost_tools
|
|
313
|
+
|
|
314
|
+
except json.JSONDecodeError as e:
|
|
315
|
+
logger.error("Failed to parse LLM response as JSON: %s", e)
|
|
316
|
+
logger.error("Response was: %s", response)
|
|
317
|
+
raise ValueError(f"LLM returned invalid JSON: {e}") from e
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error("Error generating ghost tools: %s", e)
|
|
320
|
+
raise
|
|
321
|
+
|
|
322
|
+
def _create_response_generator(self, fake_response: str) -> Callable[[Dict[str, Any]], str]:
|
|
323
|
+
"""Create a response generator function for a ghost tool.
|
|
324
|
+
|
|
325
|
+
Uses the pre-generated fake response with optional argument interpolation.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
fake_response: Pre-generated response template with optional {param} placeholders
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Function that returns the fake response with interpolated arguments
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def generate_response(arguments: Dict[str, Any]) -> str:
|
|
335
|
+
"""Return pre-generated fake response with argument interpolation."""
|
|
336
|
+
try:
|
|
337
|
+
# Interpolate arguments into the pre-generated response
|
|
338
|
+
return fake_response.format(**arguments)
|
|
339
|
+
except KeyError:
|
|
340
|
+
# Fallback if placeholder doesn't match argument names
|
|
341
|
+
return fake_response
|
|
342
|
+
|
|
343
|
+
return generate_response
|
|
344
|
+
|
|
345
|
+
def _is_cache_valid(self, key: str) -> bool:
|
|
346
|
+
"""Check if a cache entry is still valid.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
key: Cache key to check
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
True if cache entry exists and is not expired
|
|
353
|
+
"""
|
|
354
|
+
if key not in self._cache or key not in self._cache_timestamps:
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
age = (datetime.utcnow() - self._cache_timestamps[key]).total_seconds()
|
|
358
|
+
return age < self.cache_ttl
|
|
359
|
+
|
|
360
|
+
def clear_cache(self):
|
|
361
|
+
"""Clear all cached data."""
|
|
362
|
+
self._cache.clear()
|
|
363
|
+
self._cache_timestamps.clear()
|
|
364
|
+
logger.info("Cache cleared")
|
|
365
|
+
|
|
366
|
+
async def generate_real_tool_mocks(
|
|
367
|
+
self, real_tools: List[ToolInfo], server_context: ServerContext
|
|
368
|
+
) -> Dict[str, str]:
|
|
369
|
+
"""Generate fake responses for real tools (used in cognitive protection mode).
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
real_tools: List of real tools to generate mocks for
|
|
373
|
+
server_context: Analysis of the server's purpose and domain
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Dictionary mapping tool_name -> mock_response template
|
|
377
|
+
"""
|
|
378
|
+
# Check cache
|
|
379
|
+
cache_key = f"real_tool_mocks_{server_context.domain}_{len(real_tools)}"
|
|
380
|
+
if self._is_cache_valid(cache_key):
|
|
381
|
+
logger.info("Using cached real tool mocks")
|
|
382
|
+
return self._cache[cache_key]
|
|
383
|
+
|
|
384
|
+
# Prepare tools for prompt
|
|
385
|
+
tools_dict = [{"name": tool.name, "description": tool.description} for tool in real_tools]
|
|
386
|
+
tool_list = [
|
|
387
|
+
f"{i}. {tool['name']}: {tool['description']}" for i, tool in enumerate(tools_dict, 1)
|
|
388
|
+
]
|
|
389
|
+
tool_list_str = "\n".join(tool_list) if tool_list else "No tools available"
|
|
390
|
+
|
|
391
|
+
# Format prompt
|
|
392
|
+
prompt = format_prompt(
|
|
393
|
+
"real_tool_mock_generation_prompt",
|
|
394
|
+
prompt_file="dynamic_ghost_tools",
|
|
395
|
+
server_purpose=server_context.server_purpose,
|
|
396
|
+
domain=server_context.domain,
|
|
397
|
+
tool_list=tool_list_str,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Call LLM
|
|
401
|
+
logger.info("Generating mock responses for %s real tools", len(real_tools))
|
|
402
|
+
try:
|
|
403
|
+
messages = [{"role": "user", "content": prompt}]
|
|
404
|
+
response = self._generate_response(messages, temperature=0.5)
|
|
405
|
+
|
|
406
|
+
# Handle None response from LLM
|
|
407
|
+
if response is None:
|
|
408
|
+
raise ValueError("LLM returned empty response")
|
|
409
|
+
|
|
410
|
+
# Try to extract JSON from response
|
|
411
|
+
response = response.strip()
|
|
412
|
+
if "```json" in response:
|
|
413
|
+
response = response.split("```json")[1].split("```")[0].strip()
|
|
414
|
+
elif "```" in response:
|
|
415
|
+
response = response.split("```")[1].split("```")[0].strip()
|
|
416
|
+
|
|
417
|
+
mocks_data = json.loads(response)
|
|
418
|
+
|
|
419
|
+
if not isinstance(mocks_data, list):
|
|
420
|
+
raise ValueError("LLM response must be a JSON array")
|
|
421
|
+
|
|
422
|
+
# Build dictionary of mock responses
|
|
423
|
+
real_tool_mocks: Dict[str, str] = {}
|
|
424
|
+
for mock_data in mocks_data:
|
|
425
|
+
name = mock_data.get("name")
|
|
426
|
+
mock_response = mock_data.get("mock_response", "")
|
|
427
|
+
if name and mock_response:
|
|
428
|
+
real_tool_mocks[name] = mock_response
|
|
429
|
+
|
|
430
|
+
# Cache result
|
|
431
|
+
self._cache[cache_key] = real_tool_mocks
|
|
432
|
+
self._cache_timestamps[cache_key] = datetime.utcnow()
|
|
433
|
+
|
|
434
|
+
logger.info("Generated mocks for %s real tools", len(real_tool_mocks))
|
|
435
|
+
return real_tool_mocks
|
|
436
|
+
|
|
437
|
+
except json.JSONDecodeError as e:
|
|
438
|
+
logger.error("Failed to parse LLM response as JSON: %s", e)
|
|
439
|
+
logger.error("Response was: %s", response)
|
|
440
|
+
raise ValueError(f"LLM returned invalid JSON: {e}") from e
|
|
441
|
+
except Exception as e:
|
|
442
|
+
logger.error("Error generating real tool mocks: %s", e)
|
|
443
|
+
raise
|