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.
- honeymcp/api/__init__.py +1 -0
- honeymcp/api/app.py +233 -0
- honeymcp/cli.py +48 -0
- honeymcp/cli_tool_creator.py +110 -0
- honeymcp/core/catalog_updater.py +290 -0
- honeymcp/core/fingerprinter.py +3 -2
- honeymcp/core/ghost_tools.py +437 -0
- honeymcp/core/middleware.py +60 -2
- honeymcp/core/tool_creator.py +499 -0
- honeymcp/dashboard/react_umd/app.js +414 -0
- honeymcp/dashboard/react_umd/index.html +24 -0
- honeymcp/dashboard/react_umd/styles.css +535 -0
- honeymcp/storage/event_store.py +35 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.4.dist-info}/METADATA +86 -180
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.4.dist-info}/RECORD +18 -11
- honeymcp/dashboard/app.py +0 -228
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.4.dist-info}/WHEEL +0 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.4.dist-info}/entry_points.txt +0 -0
- {honeymcp-0.1.2.dist-info → honeymcp-0.1.4.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,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())
|