honeymcp 0.1.2__py3-none-any.whl → 0.1.4__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,233 @@
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 clear_events, 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
+ class ClearEventsResponse(BaseModel):
52
+ """Response returned when stored events are deleted."""
53
+
54
+ deleted_events: int
55
+ storage_path: str
56
+
57
+
58
+ def _apply_filters(
59
+ events: List[AttackFingerprint],
60
+ threat_level: Optional[str],
61
+ category: Optional[str],
62
+ tool: Optional[str],
63
+ ) -> List[AttackFingerprint]:
64
+ filtered = events
65
+ if threat_level:
66
+ filtered = [
67
+ event
68
+ for event in filtered
69
+ if event.threat_level.lower() == threat_level.lower()
70
+ ]
71
+ if category:
72
+ filtered = [event for event in filtered if event.attack_category == category]
73
+ if tool:
74
+ filtered = [event for event in filtered if event.ghost_tool_called == tool]
75
+ return filtered
76
+
77
+
78
+ def _metrics(events: List[AttackFingerprint]) -> MetricsResponse:
79
+ now = datetime.utcnow()
80
+ total_attacks = len(events)
81
+ attacks_last_24h = len([event for event in events if now - event.timestamp < timedelta(days=1)])
82
+ critical_threats = len([event for event in events if event.threat_level == "critical"])
83
+ unique_tools = len({event.ghost_tool_called for event in events})
84
+ unique_sessions = len({event.session_id for event in events})
85
+
86
+ by_threat_level: Dict[str, int] = {}
87
+ by_category: Dict[str, int] = {}
88
+ for event in events:
89
+ by_threat_level[event.threat_level] = by_threat_level.get(event.threat_level, 0) + 1
90
+ by_category[event.attack_category] = by_category.get(event.attack_category, 0) + 1
91
+
92
+ return MetricsResponse(
93
+ total_attacks=total_attacks,
94
+ attacks_last_24h=attacks_last_24h,
95
+ critical_threats=critical_threats,
96
+ unique_tools=unique_tools,
97
+ unique_sessions=unique_sessions,
98
+ by_threat_level=by_threat_level,
99
+ by_category=by_category,
100
+ )
101
+
102
+
103
+ def _configure_cors(app: FastAPI) -> None:
104
+ origins_raw = os.getenv("HONEYMCP_API_CORS_ORIGINS", "").strip()
105
+ if not origins_raw:
106
+ return
107
+ origins = [origin.strip() for origin in origins_raw.split(",") if origin.strip()]
108
+ if not origins:
109
+ return
110
+ app.add_middleware(
111
+ CORSMiddleware,
112
+ allow_origins=origins,
113
+ allow_credentials=True,
114
+ allow_methods=["*"],
115
+ allow_headers=["*"],
116
+ )
117
+
118
+
119
+ def create_app(config_path: Optional[Path | str] = None) -> FastAPI:
120
+ """Create a FastAPI app configured with HoneyMCP settings."""
121
+ config = HoneyMCPConfig.load(config_path)
122
+
123
+ app = FastAPI(
124
+ title="HoneyMCP API",
125
+ version="0.1.0",
126
+ description="HTTP API for HoneyMCP dashboard consumption",
127
+ )
128
+ _configure_cors(app)
129
+
130
+ app.state.config = config
131
+ app.state.event_storage_path = config.event_storage_path
132
+ dashboard_root = Path(__file__).resolve().parent.parent / "dashboard" / "react_umd"
133
+ app.state.dashboard_root = dashboard_root
134
+
135
+ if dashboard_root.exists():
136
+ app.mount(
137
+ "/dashboard/assets",
138
+ StaticFiles(directory=str(dashboard_root)),
139
+ name="dashboard_assets",
140
+ )
141
+
142
+ @app.get("/health")
143
+ async def health() -> Dict[str, str]:
144
+ return {"status": "ok", "timestamp": datetime.utcnow().isoformat() + "Z"}
145
+
146
+ @app.get("/dashboard")
147
+ @app.get("/dashboard/", include_in_schema=False)
148
+ async def react_dashboard() -> FileResponse:
149
+ index_path = app.state.dashboard_root / "index.html"
150
+ if not index_path.exists():
151
+ raise HTTPException(status_code=404, detail="Dashboard UI not found")
152
+ return FileResponse(index_path)
153
+
154
+ @app.get("/events", response_model=EventListResponse)
155
+ async def get_events(
156
+ start_date: Optional[date] = Query(default=None),
157
+ end_date: Optional[date] = Query(default=None),
158
+ threat_level: Optional[str] = Query(default=None),
159
+ category: Optional[str] = Query(default=None),
160
+ tool: Optional[str] = Query(default=None),
161
+ limit: int = Query(default=200, ge=1, le=1000),
162
+ offset: int = Query(default=0, ge=0),
163
+ ) -> EventListResponse:
164
+ events = await list_events(
165
+ storage_path=app.state.event_storage_path,
166
+ start_date=start_date,
167
+ end_date=end_date,
168
+ )
169
+ filtered = _apply_filters(events, threat_level, category, tool)
170
+ total = len(filtered)
171
+ page = filtered[offset : offset + limit]
172
+ return EventListResponse(
173
+ total=total,
174
+ count=len(page),
175
+ offset=offset,
176
+ limit=limit,
177
+ events=page,
178
+ )
179
+
180
+ @app.get("/events/{event_id}", response_model=AttackFingerprint)
181
+ async def get_event_by_id(event_id: str) -> AttackFingerprint:
182
+ event = await get_event(event_id, storage_path=app.state.event_storage_path)
183
+ if event is None:
184
+ raise HTTPException(status_code=404, detail="Event not found")
185
+ return event
186
+
187
+ @app.delete("/events", response_model=ClearEventsResponse)
188
+ async def delete_events() -> ClearEventsResponse:
189
+ deleted_count = await clear_events(storage_path=app.state.event_storage_path)
190
+ return ClearEventsResponse(
191
+ deleted_events=deleted_count,
192
+ storage_path=str(app.state.event_storage_path),
193
+ )
194
+
195
+ @app.get("/metrics", response_model=MetricsResponse)
196
+ async def get_metrics(
197
+ start_date: Optional[date] = Query(default=None),
198
+ end_date: Optional[date] = Query(default=None),
199
+ threat_level: Optional[str] = Query(default=None),
200
+ category: Optional[str] = Query(default=None),
201
+ tool: Optional[str] = Query(default=None),
202
+ ) -> MetricsResponse:
203
+ events = await list_events(
204
+ storage_path=app.state.event_storage_path,
205
+ start_date=start_date,
206
+ end_date=end_date,
207
+ )
208
+ filtered = _apply_filters(events, threat_level, category, tool)
209
+ return _metrics(filtered)
210
+
211
+ @app.get("/filters", response_model=FiltersResponse)
212
+ async def get_filters(
213
+ start_date: Optional[date] = Query(default=None),
214
+ end_date: Optional[date] = Query(default=None),
215
+ ) -> FiltersResponse:
216
+ events = await list_events(
217
+ storage_path=app.state.event_storage_path,
218
+ start_date=start_date,
219
+ end_date=end_date,
220
+ )
221
+ threat_levels = sorted({event.threat_level for event in events})
222
+ categories = sorted({event.attack_category for event in events})
223
+ tools = sorted({event.ghost_tool_called for event in events})
224
+ return FiltersResponse(
225
+ threat_levels=threat_levels,
226
+ categories=categories,
227
+ tools=tools,
228
+ )
229
+
230
+ return app
231
+
232
+
233
+ app = create_app()
honeymcp/cli.py CHANGED
@@ -1,9 +1,13 @@
1
1
  """HoneyMCP CLI - Command line tools for HoneyMCP setup and management."""
2
2
 
3
+ import asyncio
3
4
  import argparse
4
5
  import sys
5
6
  from pathlib import Path
6
7
 
8
+ from honeymcp.models.config import HoneyMCPConfig
9
+ from honeymcp.storage.event_store import clear_events
10
+
7
11
  CONFIG_TEMPLATE = """\
8
12
  # HoneyMCP Configuration
9
13
  # ======================
@@ -156,6 +160,27 @@ def cmd_version(args: argparse.Namespace) -> int:
156
160
  return 0
157
161
 
158
162
 
163
+ def cmd_clean_data(args: argparse.Namespace) -> int:
164
+ """Delete stored HoneyMCP attack events."""
165
+ storage_path: Path
166
+ if args.path:
167
+ storage_path = Path(args.path).expanduser()
168
+ else:
169
+ config = HoneyMCPConfig.load(args.config)
170
+ storage_path = config.event_storage_path
171
+
172
+ if not args.yes:
173
+ print(f"This will permanently delete all stored events in: {storage_path}")
174
+ confirm = input("Continue? [y/N]: ").strip().lower()
175
+ if confirm not in {"y", "yes"}:
176
+ print("Aborted.")
177
+ return 0
178
+
179
+ deleted_count = asyncio.run(clear_events(storage_path=storage_path))
180
+ print(f"Deleted {deleted_count} event file(s) from {storage_path}")
181
+ return 0
182
+
183
+
159
184
  def main() -> int:
160
185
  """Main CLI entry point."""
161
186
  parser = argparse.ArgumentParser(
@@ -191,6 +216,29 @@ def main() -> int:
191
216
  )
192
217
  version_parser.set_defaults(func=cmd_version)
193
218
 
219
+ clean_data_parser = subparsers.add_parser(
220
+ "clean-data",
221
+ help="Delete all stored attack event data",
222
+ description="Remove persisted event JSON files from HoneyMCP storage",
223
+ )
224
+ clean_data_parser.add_argument(
225
+ "--path",
226
+ default=None,
227
+ help="Event storage directory (overrides config and env resolution)",
228
+ )
229
+ clean_data_parser.add_argument(
230
+ "--config",
231
+ default=None,
232
+ help="Path to honeymcp.yaml (optional, default lookup order applies)",
233
+ )
234
+ clean_data_parser.add_argument(
235
+ "-y",
236
+ "--yes",
237
+ action="store_true",
238
+ help="Skip confirmation prompt",
239
+ )
240
+ clean_data_parser.set_defaults(func=cmd_clean_data)
241
+
194
242
  # Parse and execute
195
243
  args = parser.parse_args()
196
244
 
@@ -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())