honeymcp 0.1.1__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.
@@ -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, []