honeymcp 0.1.2__py3-none-any.whl → 0.1.3__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/api/__init__.py +1 -0
- honeymcp/api/app.py +218 -0
- honeymcp/cli_tool_creator.py +110 -0
- honeymcp/core/catalog_updater.py +290 -0
- honeymcp/core/ghost_tools.py +437 -0
- honeymcp/core/middleware.py +57 -0
- honeymcp/core/tool_creator.py +499 -0
- honeymcp/dashboard/react_umd/app.js +375 -0
- honeymcp/dashboard/react_umd/index.html +24 -0
- honeymcp/dashboard/react_umd/styles.css +512 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/METADATA +148 -27
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/RECORD +15 -8
- honeymcp/dashboard/app.py +0 -228
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/WHEEL +0 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/entry_points.txt +0 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.3.dist-info}/licenses/LICENSE +0 -0
honeymcp/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI application for HoneyMCP."""
|
honeymcp/api/app.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""HoneyMCP FastAPI service for dashboard consumption."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from datetime import date, datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI, HTTPException, Query
|
|
11
|
+
from fastapi.responses import FileResponse
|
|
12
|
+
from fastapi.staticfiles import StaticFiles
|
|
13
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from honeymcp.models.config import HoneyMCPConfig
|
|
17
|
+
from honeymcp.models.events import AttackFingerprint
|
|
18
|
+
from honeymcp.storage.event_store import get_event, list_events
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventListResponse(BaseModel):
|
|
22
|
+
"""Paginated event list response."""
|
|
23
|
+
|
|
24
|
+
total: int = Field(description="Total events matching the filters")
|
|
25
|
+
count: int = Field(description="Number of events returned in this page")
|
|
26
|
+
offset: int = Field(description="Offset for pagination")
|
|
27
|
+
limit: int = Field(description="Limit for pagination")
|
|
28
|
+
events: List[AttackFingerprint] = Field(description="Event records")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MetricsResponse(BaseModel):
|
|
32
|
+
"""Aggregate metrics for dashboard summaries."""
|
|
33
|
+
|
|
34
|
+
total_attacks: int
|
|
35
|
+
attacks_last_24h: int
|
|
36
|
+
critical_threats: int
|
|
37
|
+
unique_tools: int
|
|
38
|
+
unique_sessions: int
|
|
39
|
+
by_threat_level: Dict[str, int]
|
|
40
|
+
by_category: Dict[str, int]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FiltersResponse(BaseModel):
|
|
44
|
+
"""Distinct filter values for UI dropdowns."""
|
|
45
|
+
|
|
46
|
+
threat_levels: List[str]
|
|
47
|
+
categories: List[str]
|
|
48
|
+
tools: List[str]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _apply_filters(
|
|
52
|
+
events: List[AttackFingerprint],
|
|
53
|
+
threat_level: Optional[str],
|
|
54
|
+
category: Optional[str],
|
|
55
|
+
tool: Optional[str],
|
|
56
|
+
) -> List[AttackFingerprint]:
|
|
57
|
+
filtered = events
|
|
58
|
+
if threat_level:
|
|
59
|
+
filtered = [
|
|
60
|
+
event
|
|
61
|
+
for event in filtered
|
|
62
|
+
if event.threat_level.lower() == threat_level.lower()
|
|
63
|
+
]
|
|
64
|
+
if category:
|
|
65
|
+
filtered = [event for event in filtered if event.attack_category == category]
|
|
66
|
+
if tool:
|
|
67
|
+
filtered = [event for event in filtered if event.ghost_tool_called == tool]
|
|
68
|
+
return filtered
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _metrics(events: List[AttackFingerprint]) -> MetricsResponse:
|
|
72
|
+
now = datetime.utcnow()
|
|
73
|
+
total_attacks = len(events)
|
|
74
|
+
attacks_last_24h = len([event for event in events if now - event.timestamp < timedelta(days=1)])
|
|
75
|
+
critical_threats = len([event for event in events if event.threat_level == "critical"])
|
|
76
|
+
unique_tools = len({event.ghost_tool_called for event in events})
|
|
77
|
+
unique_sessions = len({event.session_id for event in events})
|
|
78
|
+
|
|
79
|
+
by_threat_level: Dict[str, int] = {}
|
|
80
|
+
by_category: Dict[str, int] = {}
|
|
81
|
+
for event in events:
|
|
82
|
+
by_threat_level[event.threat_level] = by_threat_level.get(event.threat_level, 0) + 1
|
|
83
|
+
by_category[event.attack_category] = by_category.get(event.attack_category, 0) + 1
|
|
84
|
+
|
|
85
|
+
return MetricsResponse(
|
|
86
|
+
total_attacks=total_attacks,
|
|
87
|
+
attacks_last_24h=attacks_last_24h,
|
|
88
|
+
critical_threats=critical_threats,
|
|
89
|
+
unique_tools=unique_tools,
|
|
90
|
+
unique_sessions=unique_sessions,
|
|
91
|
+
by_threat_level=by_threat_level,
|
|
92
|
+
by_category=by_category,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _configure_cors(app: FastAPI) -> None:
|
|
97
|
+
origins_raw = os.getenv("HONEYMCP_API_CORS_ORIGINS", "").strip()
|
|
98
|
+
if not origins_raw:
|
|
99
|
+
return
|
|
100
|
+
origins = [origin.strip() for origin in origins_raw.split(",") if origin.strip()]
|
|
101
|
+
if not origins:
|
|
102
|
+
return
|
|
103
|
+
app.add_middleware(
|
|
104
|
+
CORSMiddleware,
|
|
105
|
+
allow_origins=origins,
|
|
106
|
+
allow_credentials=True,
|
|
107
|
+
allow_methods=["*"],
|
|
108
|
+
allow_headers=["*"],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def create_app(config_path: Optional[Path | str] = None) -> FastAPI:
|
|
113
|
+
"""Create a FastAPI app configured with HoneyMCP settings."""
|
|
114
|
+
config = HoneyMCPConfig.load(config_path)
|
|
115
|
+
|
|
116
|
+
app = FastAPI(
|
|
117
|
+
title="HoneyMCP API",
|
|
118
|
+
version="0.1.0",
|
|
119
|
+
description="HTTP API for HoneyMCP dashboard consumption",
|
|
120
|
+
)
|
|
121
|
+
_configure_cors(app)
|
|
122
|
+
|
|
123
|
+
app.state.config = config
|
|
124
|
+
app.state.event_storage_path = config.event_storage_path
|
|
125
|
+
dashboard_root = Path(__file__).resolve().parent.parent / "dashboard" / "react_umd"
|
|
126
|
+
app.state.dashboard_root = dashboard_root
|
|
127
|
+
|
|
128
|
+
if dashboard_root.exists():
|
|
129
|
+
app.mount(
|
|
130
|
+
"/dashboard/assets",
|
|
131
|
+
StaticFiles(directory=str(dashboard_root)),
|
|
132
|
+
name="dashboard_assets",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@app.get("/health")
|
|
136
|
+
async def health() -> Dict[str, str]:
|
|
137
|
+
return {"status": "ok", "timestamp": datetime.utcnow().isoformat() + "Z"}
|
|
138
|
+
|
|
139
|
+
@app.get("/dashboard")
|
|
140
|
+
@app.get("/dashboard/", include_in_schema=False)
|
|
141
|
+
async def react_dashboard() -> FileResponse:
|
|
142
|
+
index_path = app.state.dashboard_root / "index.html"
|
|
143
|
+
if not index_path.exists():
|
|
144
|
+
raise HTTPException(status_code=404, detail="Dashboard UI not found")
|
|
145
|
+
return FileResponse(index_path)
|
|
146
|
+
|
|
147
|
+
@app.get("/events", response_model=EventListResponse)
|
|
148
|
+
async def get_events(
|
|
149
|
+
start_date: Optional[date] = Query(default=None),
|
|
150
|
+
end_date: Optional[date] = Query(default=None),
|
|
151
|
+
threat_level: Optional[str] = Query(default=None),
|
|
152
|
+
category: Optional[str] = Query(default=None),
|
|
153
|
+
tool: Optional[str] = Query(default=None),
|
|
154
|
+
limit: int = Query(default=200, ge=1, le=1000),
|
|
155
|
+
offset: int = Query(default=0, ge=0),
|
|
156
|
+
) -> EventListResponse:
|
|
157
|
+
events = await list_events(
|
|
158
|
+
storage_path=app.state.event_storage_path,
|
|
159
|
+
start_date=start_date,
|
|
160
|
+
end_date=end_date,
|
|
161
|
+
)
|
|
162
|
+
filtered = _apply_filters(events, threat_level, category, tool)
|
|
163
|
+
total = len(filtered)
|
|
164
|
+
page = filtered[offset : offset + limit]
|
|
165
|
+
return EventListResponse(
|
|
166
|
+
total=total,
|
|
167
|
+
count=len(page),
|
|
168
|
+
offset=offset,
|
|
169
|
+
limit=limit,
|
|
170
|
+
events=page,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@app.get("/events/{event_id}", response_model=AttackFingerprint)
|
|
174
|
+
async def get_event_by_id(event_id: str) -> AttackFingerprint:
|
|
175
|
+
event = await get_event(event_id, storage_path=app.state.event_storage_path)
|
|
176
|
+
if event is None:
|
|
177
|
+
raise HTTPException(status_code=404, detail="Event not found")
|
|
178
|
+
return event
|
|
179
|
+
|
|
180
|
+
@app.get("/metrics", response_model=MetricsResponse)
|
|
181
|
+
async def get_metrics(
|
|
182
|
+
start_date: Optional[date] = Query(default=None),
|
|
183
|
+
end_date: Optional[date] = Query(default=None),
|
|
184
|
+
threat_level: Optional[str] = Query(default=None),
|
|
185
|
+
category: Optional[str] = Query(default=None),
|
|
186
|
+
tool: Optional[str] = Query(default=None),
|
|
187
|
+
) -> MetricsResponse:
|
|
188
|
+
events = await list_events(
|
|
189
|
+
storage_path=app.state.event_storage_path,
|
|
190
|
+
start_date=start_date,
|
|
191
|
+
end_date=end_date,
|
|
192
|
+
)
|
|
193
|
+
filtered = _apply_filters(events, threat_level, category, tool)
|
|
194
|
+
return _metrics(filtered)
|
|
195
|
+
|
|
196
|
+
@app.get("/filters", response_model=FiltersResponse)
|
|
197
|
+
async def get_filters(
|
|
198
|
+
start_date: Optional[date] = Query(default=None),
|
|
199
|
+
end_date: Optional[date] = Query(default=None),
|
|
200
|
+
) -> FiltersResponse:
|
|
201
|
+
events = await list_events(
|
|
202
|
+
storage_path=app.state.event_storage_path,
|
|
203
|
+
start_date=start_date,
|
|
204
|
+
end_date=end_date,
|
|
205
|
+
)
|
|
206
|
+
threat_levels = sorted({event.threat_level for event in events})
|
|
207
|
+
categories = sorted({event.attack_category for event in events})
|
|
208
|
+
tools = sorted({event.ghost_tool_called for event in events})
|
|
209
|
+
return FiltersResponse(
|
|
210
|
+
threat_levels=threat_levels,
|
|
211
|
+
categories=categories,
|
|
212
|
+
tools=tools,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return app
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
app = create_app()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""CLI command for creating new honeypot tools."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from honeymcp.core.tool_creator import ToolCreatorAgent
|
|
8
|
+
from honeymcp.core.catalog_updater import CatalogUpdater
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_tool_command(description: str, project_root: Optional[Path] = None) -> int:
|
|
12
|
+
"""CLI command to create a new honeypot tool.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
description: Natural language description of the tool
|
|
16
|
+
project_root: Optional project root path
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Exit code (0 for success, 1 for failure)
|
|
20
|
+
"""
|
|
21
|
+
print("🍯 HoneyMCP Tool Creator")
|
|
22
|
+
print("=" * 50)
|
|
23
|
+
print(f"\nDescription: {description}\n")
|
|
24
|
+
|
|
25
|
+
# Step 1: Create tool using ReAct agent
|
|
26
|
+
print("Step 1: Analyzing description and generating tool specification...")
|
|
27
|
+
agent = ToolCreatorAgent()
|
|
28
|
+
success, tool_spec, errors = agent.create_tool(description)
|
|
29
|
+
|
|
30
|
+
if not success:
|
|
31
|
+
print("❌ Failed to create tool specification:")
|
|
32
|
+
for error in errors:
|
|
33
|
+
print(f" - {error}")
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
print(f"✅ Tool specification created: {tool_spec.name}")
|
|
37
|
+
print(f" Category: {tool_spec.attack_category}")
|
|
38
|
+
print(f" Threat Level: {tool_spec.threat_level}")
|
|
39
|
+
|
|
40
|
+
# Show reasoning process
|
|
41
|
+
state = agent.get_state_summary()
|
|
42
|
+
if state["reasoning"]:
|
|
43
|
+
print("\n📝 Agent Reasoning:")
|
|
44
|
+
for i, thought in enumerate(state["reasoning"], 1):
|
|
45
|
+
print(f" {i}. {thought}")
|
|
46
|
+
|
|
47
|
+
if state["reflections"]:
|
|
48
|
+
print("\n🔍 Quality Reflections:")
|
|
49
|
+
for i, reflection in enumerate(state["reflections"], 1):
|
|
50
|
+
print(f" {i}. {reflection}")
|
|
51
|
+
|
|
52
|
+
# Step 2: Get response function code
|
|
53
|
+
print("\nStep 2: Extracting response generator code...")
|
|
54
|
+
# We need to regenerate the code since we can't extract it from the spec
|
|
55
|
+
# This is a limitation - in production, we'd store it in the agent
|
|
56
|
+
from honeymcp.core.tool_creator import ToolSpecification
|
|
57
|
+
|
|
58
|
+
# Recreate spec for code generation
|
|
59
|
+
temp_spec = ToolSpecification(
|
|
60
|
+
name=tool_spec.name,
|
|
61
|
+
description=tool_spec.description,
|
|
62
|
+
parameters=tool_spec.parameters,
|
|
63
|
+
required_params=tool_spec.parameters.get("required", []),
|
|
64
|
+
category=agent._determine_category(description),
|
|
65
|
+
threat_level=agent._determine_threat_level(description),
|
|
66
|
+
response_template=""
|
|
67
|
+
)
|
|
68
|
+
response_func_code = agent._generate_response_function(temp_spec)
|
|
69
|
+
print(f"✅ Response generator created ({len(response_func_code)} chars)")
|
|
70
|
+
|
|
71
|
+
# Step 3: Update catalog
|
|
72
|
+
print("\nStep 3: Adding tool to catalog...")
|
|
73
|
+
updater = CatalogUpdater(project_root)
|
|
74
|
+
success, errors = updater.add_tool_complete(tool_spec, response_func_code)
|
|
75
|
+
|
|
76
|
+
if not success:
|
|
77
|
+
print("❌ Failed to update catalog:")
|
|
78
|
+
for error in errors:
|
|
79
|
+
print(f" - {error}")
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
print("✅ Tool added to ghost_tools.py")
|
|
83
|
+
print("✅ Handler added to middleware.py")
|
|
84
|
+
|
|
85
|
+
# Step 4: Summary
|
|
86
|
+
print("\n" + "=" * 50)
|
|
87
|
+
print("✅ Tool creation complete!")
|
|
88
|
+
print(f"\nNew tool: {tool_spec.name}")
|
|
89
|
+
print(f"Description: {tool_spec.description}")
|
|
90
|
+
print(f"\nTo use this tool:")
|
|
91
|
+
print(f' mcp = honeypot(mcp, ghost_tools=["{tool_spec.name}"])')
|
|
92
|
+
print("\nOr it will be available in dynamic mode automatically.")
|
|
93
|
+
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
"""Main CLI entry point."""
|
|
99
|
+
if len(sys.argv) < 2:
|
|
100
|
+
print("Usage: honeymcp create-tool <description>")
|
|
101
|
+
print("\nExample:")
|
|
102
|
+
print(' honeymcp create-tool "dump container registry credentials"')
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
description = " ".join(sys.argv[1:])
|
|
106
|
+
return create_tool_command(description)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(main())
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Catalog updater for adding new honeypot tools to ghost_tools.py and middleware.py"""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Tuple, List, Optional
|
|
7
|
+
|
|
8
|
+
from honeymcp.models.ghost_tool_spec import GhostToolSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CatalogUpdater:
|
|
12
|
+
"""Updates ghost_tools.py and middleware.py with new honeypot tools."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
15
|
+
"""Initialize catalog updater.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
project_root: Root directory of HoneyMCP project. If None, auto-detects.
|
|
19
|
+
"""
|
|
20
|
+
if project_root is None:
|
|
21
|
+
# Auto-detect project root
|
|
22
|
+
current = Path(__file__).resolve()
|
|
23
|
+
while current.parent != current:
|
|
24
|
+
if (current / "src" / "honeymcp").exists():
|
|
25
|
+
project_root = current
|
|
26
|
+
break
|
|
27
|
+
current = current.parent
|
|
28
|
+
|
|
29
|
+
self.project_root = project_root
|
|
30
|
+
self.ghost_tools_path = project_root / "src" / "honeymcp" / "core" / "ghost_tools.py"
|
|
31
|
+
self.middleware_path = project_root / "src" / "honeymcp" / "core" / "middleware.py"
|
|
32
|
+
|
|
33
|
+
def add_tool_to_catalog(
|
|
34
|
+
self,
|
|
35
|
+
tool_spec: GhostToolSpec,
|
|
36
|
+
response_func_code: str
|
|
37
|
+
) -> Tuple[bool, List[str]]:
|
|
38
|
+
"""Add new tool to ghost_tools.py catalog.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
tool_spec: GhostToolSpec for the new tool
|
|
42
|
+
response_func_code: Python code for response generator function
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Tuple of (success, errors)
|
|
46
|
+
"""
|
|
47
|
+
errors = []
|
|
48
|
+
|
|
49
|
+
# Step 1: Validate tool doesn't already exist
|
|
50
|
+
if self._tool_exists(tool_spec.name):
|
|
51
|
+
errors.append(f"Tool '{tool_spec.name}' already exists in catalog")
|
|
52
|
+
return False, errors
|
|
53
|
+
|
|
54
|
+
# Step 2: Read current ghost_tools.py
|
|
55
|
+
try:
|
|
56
|
+
content = self.ghost_tools_path.read_text()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
errors.append(f"Failed to read ghost_tools.py: {e}")
|
|
59
|
+
return False, errors
|
|
60
|
+
|
|
61
|
+
# Step 3: Add response generator function
|
|
62
|
+
updated_content = self._add_response_generator(content, response_func_code)
|
|
63
|
+
|
|
64
|
+
# Step 4: Add tool to GHOST_TOOL_CATALOG
|
|
65
|
+
updated_content = self._add_to_catalog(updated_content, tool_spec)
|
|
66
|
+
|
|
67
|
+
# Step 5: Validate syntax
|
|
68
|
+
try:
|
|
69
|
+
ast.parse(updated_content)
|
|
70
|
+
except SyntaxError as e:
|
|
71
|
+
errors.append(f"Generated code has syntax error: {e}")
|
|
72
|
+
return False, errors
|
|
73
|
+
|
|
74
|
+
# Step 6: Write updated file
|
|
75
|
+
try:
|
|
76
|
+
self.ghost_tools_path.write_text(updated_content)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
errors.append(f"Failed to write ghost_tools.py: {e}")
|
|
79
|
+
return False, errors
|
|
80
|
+
|
|
81
|
+
return True, []
|
|
82
|
+
|
|
83
|
+
def add_tool_to_middleware(self, tool_spec: GhostToolSpec) -> Tuple[bool, List[str]]:
|
|
84
|
+
"""Add new tool handler to middleware.py.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
tool_spec: GhostToolSpec for the new tool
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple of (success, errors)
|
|
91
|
+
"""
|
|
92
|
+
errors = []
|
|
93
|
+
|
|
94
|
+
# Step 1: Read current middleware.py
|
|
95
|
+
try:
|
|
96
|
+
content = self.middleware_path.read_text()
|
|
97
|
+
except Exception as e:
|
|
98
|
+
errors.append(f"Failed to read middleware.py: {e}")
|
|
99
|
+
return False, errors
|
|
100
|
+
|
|
101
|
+
# Step 2: Generate handler code
|
|
102
|
+
handler_code = self._generate_handler_code(tool_spec)
|
|
103
|
+
|
|
104
|
+
# Step 3: Insert handler before "else:" clause
|
|
105
|
+
updated_content = self._insert_handler(content, handler_code)
|
|
106
|
+
|
|
107
|
+
# Step 4: Validate syntax
|
|
108
|
+
try:
|
|
109
|
+
ast.parse(updated_content)
|
|
110
|
+
except SyntaxError as e:
|
|
111
|
+
errors.append(f"Generated middleware code has syntax error: {e}")
|
|
112
|
+
return False, errors
|
|
113
|
+
|
|
114
|
+
# Step 5: Write updated file
|
|
115
|
+
try:
|
|
116
|
+
self.middleware_path.write_text(updated_content)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
errors.append(f"Failed to write middleware.py: {e}")
|
|
119
|
+
return False, errors
|
|
120
|
+
|
|
121
|
+
return True, []
|
|
122
|
+
|
|
123
|
+
def _tool_exists(self, tool_name: str) -> bool:
|
|
124
|
+
"""Check if tool already exists in catalog."""
|
|
125
|
+
content = self.ghost_tools_path.read_text()
|
|
126
|
+
return f'"{tool_name}": GhostToolSpec(' in content
|
|
127
|
+
|
|
128
|
+
def _add_response_generator(self, content: str, func_code: str) -> str:
|
|
129
|
+
"""Add response generator function to ghost_tools.py."""
|
|
130
|
+
# Find the last response generator function
|
|
131
|
+
last_func_pattern = r'(def generate_fake_\w+\(args: Dict\[str, Any\]\) -> str:.*?(?=\n\ndef |\n\n# Ghost tool catalog))'
|
|
132
|
+
matches = list(re.finditer(last_func_pattern, content, re.DOTALL))
|
|
133
|
+
|
|
134
|
+
if matches:
|
|
135
|
+
last_match = matches[-1]
|
|
136
|
+
insert_pos = last_match.end()
|
|
137
|
+
# Add new function after last one
|
|
138
|
+
return content[:insert_pos] + "\n\n" + func_code.rstrip() + "\n" + content[insert_pos:]
|
|
139
|
+
else:
|
|
140
|
+
# No functions found, add before catalog
|
|
141
|
+
catalog_pos = content.find("# Ghost tool catalog")
|
|
142
|
+
if catalog_pos > 0:
|
|
143
|
+
return content[:catalog_pos] + func_code.rstrip() + "\n\n" + content[catalog_pos:]
|
|
144
|
+
|
|
145
|
+
return content
|
|
146
|
+
|
|
147
|
+
def _add_to_catalog(self, content: str, tool_spec: GhostToolSpec) -> str:
|
|
148
|
+
"""Add tool entry to GHOST_TOOL_CATALOG dictionary."""
|
|
149
|
+
# Generate catalog entry
|
|
150
|
+
catalog_entry = self._generate_catalog_entry(tool_spec)
|
|
151
|
+
|
|
152
|
+
# Find the closing brace of GHOST_TOOL_CATALOG
|
|
153
|
+
# Look for the last tool entry before the closing brace
|
|
154
|
+
pattern = r'( "[\w_]+": GhostToolSpec\(.*?\),)\n(\})'
|
|
155
|
+
|
|
156
|
+
def replacer(match):
|
|
157
|
+
last_entry = match.group(1)
|
|
158
|
+
closing_brace = match.group(2)
|
|
159
|
+
return f"{last_entry}\n{catalog_entry}\n{closing_brace}"
|
|
160
|
+
|
|
161
|
+
updated = re.sub(pattern, replacer, content, flags=re.DOTALL)
|
|
162
|
+
|
|
163
|
+
return updated
|
|
164
|
+
|
|
165
|
+
def _generate_catalog_entry(self, tool_spec: GhostToolSpec) -> str:
|
|
166
|
+
"""Generate catalog entry code for tool."""
|
|
167
|
+
# Format parameters as Python dict
|
|
168
|
+
params_str = str(tool_spec.parameters).replace("'", '"')
|
|
169
|
+
|
|
170
|
+
entry = f''' "{tool_spec.name}": GhostToolSpec(
|
|
171
|
+
name="{tool_spec.name}",
|
|
172
|
+
description="{tool_spec.description}",
|
|
173
|
+
parameters={params_str},
|
|
174
|
+
response_generator=generate_fake_{tool_spec.name},
|
|
175
|
+
threat_level="{tool_spec.threat_level}",
|
|
176
|
+
attack_category="{tool_spec.attack_category}",
|
|
177
|
+
),'''
|
|
178
|
+
|
|
179
|
+
return entry
|
|
180
|
+
|
|
181
|
+
def _generate_handler_code(self, tool_spec: GhostToolSpec) -> str:
|
|
182
|
+
"""Generate middleware handler code for tool."""
|
|
183
|
+
# Extract parameters
|
|
184
|
+
properties = tool_spec.parameters.get("properties", {})
|
|
185
|
+
required = tool_spec.parameters.get("required", [])
|
|
186
|
+
|
|
187
|
+
# Build parameter list
|
|
188
|
+
params = []
|
|
189
|
+
for param_name, param_spec in properties.items():
|
|
190
|
+
param_type = self._python_type_from_json_type(param_spec.get("type", "string"))
|
|
191
|
+
if param_name in required:
|
|
192
|
+
params.append(f"{param_name}: {param_type}")
|
|
193
|
+
else:
|
|
194
|
+
default = self._get_param_default(param_name, param_spec)
|
|
195
|
+
params.append(f"{param_name}: {param_type} = {default}")
|
|
196
|
+
|
|
197
|
+
params_str = ", ".join(params) if params else ""
|
|
198
|
+
|
|
199
|
+
# Build args dict
|
|
200
|
+
if params:
|
|
201
|
+
args_dict_lines = [f' {{"{p.split(":")[0].strip()}": {p.split(":")[0].strip()}}}'
|
|
202
|
+
for p in params]
|
|
203
|
+
args_dict = "{\n" + ",\n".join([f' "{p.split(":")[0].strip()}": {p.split(":")[0].strip()}'
|
|
204
|
+
for p in params]) + "\n }"
|
|
205
|
+
else:
|
|
206
|
+
args_dict = "{}"
|
|
207
|
+
|
|
208
|
+
handler = f''' elif ghost_spec.name == "{tool_spec.name}":
|
|
209
|
+
|
|
210
|
+
@server.tool(name=ghost_spec.name, description=ghost_spec.description)
|
|
211
|
+
async def handler({params_str}):
|
|
212
|
+
"""Generated handler for {tool_spec.name} (fallback only)."""
|
|
213
|
+
return ghost_spec.response_generator({args_dict})
|
|
214
|
+
'''
|
|
215
|
+
|
|
216
|
+
return handler
|
|
217
|
+
|
|
218
|
+
def _python_type_from_json_type(self, json_type: str) -> str:
|
|
219
|
+
"""Convert JSON schema type to Python type hint."""
|
|
220
|
+
type_map = {
|
|
221
|
+
"string": "str",
|
|
222
|
+
"integer": "int",
|
|
223
|
+
"number": "float",
|
|
224
|
+
"boolean": "bool",
|
|
225
|
+
"array": "list",
|
|
226
|
+
"object": "dict"
|
|
227
|
+
}
|
|
228
|
+
return type_map.get(json_type, "str")
|
|
229
|
+
|
|
230
|
+
def _get_param_default(self, param_name: str, param_spec: dict) -> str:
|
|
231
|
+
"""Get default value for parameter."""
|
|
232
|
+
param_type = param_spec.get("type", "string")
|
|
233
|
+
|
|
234
|
+
if param_type == "integer":
|
|
235
|
+
if "limit" in param_name or "count" in param_name:
|
|
236
|
+
return "10"
|
|
237
|
+
elif "duration" in param_name:
|
|
238
|
+
return "60"
|
|
239
|
+
return "0"
|
|
240
|
+
elif param_type == "boolean":
|
|
241
|
+
return "True"
|
|
242
|
+
elif param_type == "string":
|
|
243
|
+
return f'"{param_name}_default"'
|
|
244
|
+
else:
|
|
245
|
+
return "None"
|
|
246
|
+
|
|
247
|
+
def _insert_handler(self, content: str, handler_code: str) -> str:
|
|
248
|
+
"""Insert handler code before the final else clause."""
|
|
249
|
+
# Find the last "elif ghost_spec.name ==" before "else:"
|
|
250
|
+
pattern = r'( elif ghost_spec\.name == "[\w_]+":\n.*?return ghost_spec\.response_generator\(.*?\)\n)\n( else:\n raise ValueError)'
|
|
251
|
+
|
|
252
|
+
def replacer(match):
|
|
253
|
+
last_handler = match.group(1)
|
|
254
|
+
else_clause = match.group(2)
|
|
255
|
+
return f"{last_handler}\n{handler_code}\n{else_clause}"
|
|
256
|
+
|
|
257
|
+
updated = re.sub(pattern, replacer, content, flags=re.DOTALL)
|
|
258
|
+
|
|
259
|
+
return updated
|
|
260
|
+
|
|
261
|
+
def add_tool_complete(
|
|
262
|
+
self,
|
|
263
|
+
tool_spec: GhostToolSpec,
|
|
264
|
+
response_func_code: str
|
|
265
|
+
) -> Tuple[bool, List[str]]:
|
|
266
|
+
"""Complete workflow: add tool to both catalog and middleware.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
tool_spec: GhostToolSpec for the new tool
|
|
270
|
+
response_func_code: Python code for response generator function
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Tuple of (success, errors)
|
|
274
|
+
"""
|
|
275
|
+
all_errors = []
|
|
276
|
+
|
|
277
|
+
# Step 1: Add to ghost_tools.py
|
|
278
|
+
success, errors = self.add_tool_to_catalog(tool_spec, response_func_code)
|
|
279
|
+
if not success:
|
|
280
|
+
all_errors.extend(errors)
|
|
281
|
+
return False, all_errors
|
|
282
|
+
|
|
283
|
+
# Step 2: Add to middleware.py
|
|
284
|
+
success, errors = self.add_tool_to_middleware(tool_spec)
|
|
285
|
+
if not success:
|
|
286
|
+
all_errors.extend(errors)
|
|
287
|
+
# Rollback ghost_tools.py changes would go here
|
|
288
|
+
return False, all_errors
|
|
289
|
+
|
|
290
|
+
return True, []
|