starting full refactor with FastAPI routes + HTMX and AlpineJS on client side
This commit is contained in:
5
src/routes/__init__.py
Normal file
5
src/routes/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
FastAPI routes package for the Krawl honeypot.
|
||||
"""
|
||||
321
src/routes/api.py
Normal file
321
src/routes/api.py
Normal file
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Dashboard JSON API routes.
|
||||
Migrated from handler.py dashboard API endpoints.
|
||||
All endpoints are prefixed with the secret dashboard path.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Request, Response, Query
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse
|
||||
|
||||
from dependencies import get_db
|
||||
from logger import get_app_logger
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _no_cache_headers() -> dict:
|
||||
return {
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/all-ip-stats")
|
||||
async def all_ip_stats(request: Request):
|
||||
db = get_db()
|
||||
try:
|
||||
ip_stats_list = db.get_ip_stats(limit=500)
|
||||
return JSONResponse(
|
||||
content={"ips": ip_stats_list},
|
||||
headers=_no_cache_headers(),
|
||||
)
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching all IP stats: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/attackers")
|
||||
async def attackers(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(25),
|
||||
sort_by: str = Query("total_requests"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_attackers_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching attackers: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/all-ips")
|
||||
async def all_ips(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(25),
|
||||
sort_by: str = Query("total_requests"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_all_ips_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching all IPs: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/ip-stats/{ip_address:path}")
|
||||
async def ip_stats(ip_address: str, request: Request):
|
||||
db = get_db()
|
||||
try:
|
||||
stats = db.get_ip_stats_by_ip(ip_address)
|
||||
if stats:
|
||||
return JSONResponse(content=stats, headers=_no_cache_headers())
|
||||
else:
|
||||
return JSONResponse(
|
||||
content={"error": "IP not found"}, headers=_no_cache_headers()
|
||||
)
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching IP stats: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/honeypot")
|
||||
async def honeypot(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(5),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_honeypot_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching honeypot data: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/credentials")
|
||||
async def credentials(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(5),
|
||||
sort_by: str = Query("timestamp"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_credentials_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching credentials: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/top-ips")
|
||||
async def top_ips(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(5),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_top_ips_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching top IPs: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/top-paths")
|
||||
async def top_paths(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(5),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_top_paths_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching top paths: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/top-user-agents")
|
||||
async def top_user_agents(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(5),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_top_user_agents_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching top user agents: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/attack-types-stats")
|
||||
async def attack_types_stats(
|
||||
request: Request,
|
||||
limit: int = Query(20),
|
||||
):
|
||||
db = get_db()
|
||||
limit = min(max(1, limit), 100)
|
||||
|
||||
try:
|
||||
result = db.get_attack_types_stats(limit=limit)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching attack types stats: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/attack-types")
|
||||
async def attack_types(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
page_size: int = Query(5),
|
||||
sort_by: str = Query("timestamp"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
|
||||
try:
|
||||
result = db.get_attack_types_paginated(
|
||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
return JSONResponse(content=result, headers=_no_cache_headers())
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching attack types: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, headers=_no_cache_headers())
|
||||
|
||||
|
||||
@router.get("/api/raw-request/{log_id:int}")
|
||||
async def raw_request(log_id: int, request: Request):
|
||||
db = get_db()
|
||||
try:
|
||||
raw = db.get_raw_request_by_id(log_id)
|
||||
if raw is None:
|
||||
return JSONResponse(
|
||||
content={"error": "Raw request not found"}, status_code=404
|
||||
)
|
||||
return JSONResponse(
|
||||
content={"raw_request": raw}, headers=_no_cache_headers()
|
||||
)
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error fetching raw request: {e}")
|
||||
return JSONResponse(content={"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/api/get_banlist")
|
||||
async def get_banlist(request: Request, fwtype: str = Query("iptables")):
|
||||
config = request.app.state.config
|
||||
|
||||
filename = f"{fwtype}_banlist.txt"
|
||||
if fwtype == "raw":
|
||||
filename = "malicious_ips.txt"
|
||||
|
||||
file_path = os.path.join(config.exports_path, filename)
|
||||
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, "rb") as f:
|
||||
content = f.read()
|
||||
return Response(
|
||||
content=content,
|
||||
status_code=200,
|
||||
media_type="text/plain",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||
"Content-Length": str(len(content)),
|
||||
},
|
||||
)
|
||||
else:
|
||||
return PlainTextResponse("File not found", status_code=404)
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error serving malicious IPs file: {e}")
|
||||
return PlainTextResponse("Internal server error", status_code=500)
|
||||
|
||||
|
||||
@router.get("/api/download/malicious_ips.txt")
|
||||
async def download_malicious_ips(request: Request):
|
||||
config = request.app.state.config
|
||||
file_path = os.path.join(config.exports_path, "malicious_ips.txt")
|
||||
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, "rb") as f:
|
||||
content = f.read()
|
||||
return Response(
|
||||
content=content,
|
||||
status_code=200,
|
||||
media_type="text/plain",
|
||||
headers={
|
||||
"Content-Disposition": 'attachment; filename="malicious_ips.txt"',
|
||||
"Content-Length": str(len(content)),
|
||||
},
|
||||
)
|
||||
else:
|
||||
return PlainTextResponse("File not found", status_code=404)
|
||||
except Exception as e:
|
||||
get_app_logger().error(f"Error serving malicious IPs file: {e}")
|
||||
return PlainTextResponse("Internal server error", status_code=500)
|
||||
38
src/routes/dashboard.py
Normal file
38
src/routes/dashboard.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Dashboard page route.
|
||||
Renders the main dashboard page with server-side data for initial load.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
from dependencies import get_db, get_templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def dashboard_page(request: Request):
|
||||
db = get_db()
|
||||
config = request.app.state.config
|
||||
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
||||
|
||||
# Get initial data for server-rendered sections
|
||||
stats = db.get_dashboard_counts()
|
||||
suspicious = db.get_recent_suspicious(limit=20)
|
||||
|
||||
# Get credential count for the stats card
|
||||
cred_result = db.get_credentials_paginated(page=1, page_size=1)
|
||||
stats["credential_count"] = cred_result["pagination"]["total"]
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/index.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": dashboard_path,
|
||||
"stats": stats,
|
||||
"suspicious_activities": suspicious,
|
||||
},
|
||||
)
|
||||
450
src/routes/honeypot.py
Normal file
450
src/routes/honeypot.py
Normal file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Honeypot trap routes for the Krawl deception server.
|
||||
Migrated from handler.py serve_special_path(), do_POST(), and do_GET() catch-all.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse, parse_qs, unquote_plus
|
||||
|
||||
from fastapi import APIRouter, Request, Response, Depends
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse
|
||||
|
||||
from dependencies import (
|
||||
get_tracker,
|
||||
get_app_config,
|
||||
get_client_ip,
|
||||
build_raw_request,
|
||||
)
|
||||
from config import Config
|
||||
from tracker import AccessTracker
|
||||
from templates import html_templates
|
||||
from generators import (
|
||||
credentials_txt,
|
||||
passwords_txt,
|
||||
users_json,
|
||||
api_keys_json,
|
||||
api_response,
|
||||
directory_listing,
|
||||
)
|
||||
from deception_responses import (
|
||||
generate_sql_error_response,
|
||||
get_sql_response_with_data,
|
||||
detect_xss_pattern,
|
||||
generate_xss_response,
|
||||
generate_server_error,
|
||||
)
|
||||
from wordlists import get_wordlists
|
||||
from logger import get_app_logger, get_access_logger, get_credential_logger
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# --- Helper functions ---
|
||||
|
||||
def _should_return_error(config: Config) -> bool:
|
||||
if config.probability_error_codes <= 0:
|
||||
return False
|
||||
return random.randint(1, 100) <= config.probability_error_codes
|
||||
|
||||
|
||||
def _get_random_error_code() -> int:
|
||||
wl = get_wordlists()
|
||||
error_codes = wl.error_codes
|
||||
if not error_codes:
|
||||
error_codes = [400, 401, 403, 404, 500, 502, 503]
|
||||
return random.choice(error_codes)
|
||||
|
||||
|
||||
# --- HEAD ---
|
||||
|
||||
@router.head("/{path:path}")
|
||||
async def handle_head(path: str):
|
||||
return Response(status_code=200, headers={"Content-Type": "text/html"})
|
||||
|
||||
|
||||
# --- POST routes ---
|
||||
|
||||
@router.post("/api/search")
|
||||
@router.post("/api/sql")
|
||||
@router.post("/api/database")
|
||||
async def sql_endpoint_post(request: Request):
|
||||
client_ip = get_client_ip(request)
|
||||
access_logger = get_access_logger()
|
||||
|
||||
body_bytes = await request.body()
|
||||
post_data = body_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
base_path = request.url.path
|
||||
access_logger.info(
|
||||
f"[SQL ENDPOINT POST] {client_ip} - {base_path} - Data: {post_data[:100] if post_data else 'empty'}"
|
||||
)
|
||||
|
||||
error_msg, content_type, status_code = generate_sql_error_response(post_data)
|
||||
|
||||
if error_msg:
|
||||
access_logger.warning(
|
||||
f"[SQL INJECTION DETECTED POST] {client_ip} - {base_path}"
|
||||
)
|
||||
return Response(content=error_msg, status_code=status_code, media_type=content_type)
|
||||
else:
|
||||
response_data = get_sql_response_with_data(base_path, post_data)
|
||||
return Response(content=response_data, status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
@router.post("/api/contact")
|
||||
async def contact_post(request: Request):
|
||||
client_ip = get_client_ip(request)
|
||||
user_agent = request.headers.get("User-Agent", "")
|
||||
tracker = request.app.state.tracker
|
||||
access_logger = get_access_logger()
|
||||
app_logger = get_app_logger()
|
||||
|
||||
body_bytes = await request.body()
|
||||
post_data = body_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
parsed_data = {}
|
||||
if post_data:
|
||||
parsed_qs = parse_qs(post_data)
|
||||
parsed_data = {k: v[0] if v else "" for k, v in parsed_qs.items()}
|
||||
|
||||
xss_detected = any(detect_xss_pattern(str(v)) for v in parsed_data.values())
|
||||
|
||||
if xss_detected:
|
||||
access_logger.warning(
|
||||
f"[XSS ATTEMPT DETECTED] {client_ip} - {request.url.path} - Data: {post_data[:200]}"
|
||||
)
|
||||
else:
|
||||
access_logger.info(
|
||||
f"[XSS ENDPOINT POST] {client_ip} - {request.url.path}"
|
||||
)
|
||||
|
||||
tracker.record_access(
|
||||
ip=client_ip,
|
||||
path=str(request.url.path),
|
||||
user_agent=user_agent,
|
||||
body=post_data,
|
||||
method="POST",
|
||||
raw_request=build_raw_request(request, post_data),
|
||||
)
|
||||
|
||||
response_html = generate_xss_response(parsed_data)
|
||||
return HTMLResponse(content=response_html, status_code=200)
|
||||
|
||||
|
||||
@router.post("/{path:path}")
|
||||
async def credential_capture_post(request: Request, path: str):
|
||||
"""Catch-all POST handler for credential capture."""
|
||||
client_ip = get_client_ip(request)
|
||||
user_agent = request.headers.get("User-Agent", "")
|
||||
tracker = request.app.state.tracker
|
||||
access_logger = get_access_logger()
|
||||
credential_logger = get_credential_logger()
|
||||
|
||||
body_bytes = await request.body()
|
||||
post_data = body_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
full_path = f"/{path}"
|
||||
|
||||
access_logger.warning(
|
||||
f"[LOGIN ATTEMPT] {client_ip} - {full_path} - {user_agent[:50]}"
|
||||
)
|
||||
|
||||
if post_data:
|
||||
access_logger.warning(f"[POST DATA] {post_data[:200]}")
|
||||
|
||||
username, password = tracker.parse_credentials(post_data)
|
||||
if username or password:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
credential_line = f"{timestamp}|{client_ip}|{username or 'N/A'}|{password or 'N/A'}|{full_path}"
|
||||
credential_logger.info(credential_line)
|
||||
|
||||
tracker.record_credential_attempt(
|
||||
client_ip, full_path, username or "N/A", password or "N/A"
|
||||
)
|
||||
|
||||
access_logger.warning(
|
||||
f"[CREDENTIALS CAPTURED] {client_ip} - Username: {username or 'N/A'} - Path: {full_path}"
|
||||
)
|
||||
|
||||
tracker.record_access(
|
||||
client_ip,
|
||||
full_path,
|
||||
user_agent,
|
||||
post_data,
|
||||
method="POST",
|
||||
raw_request=build_raw_request(request, post_data),
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
return HTMLResponse(content=html_templates.login_error(), status_code=200)
|
||||
|
||||
|
||||
# --- GET special paths ---
|
||||
|
||||
@router.get("/robots.txt")
|
||||
async def robots_txt():
|
||||
return PlainTextResponse(html_templates.robots_txt())
|
||||
|
||||
|
||||
@router.get("/credentials.txt")
|
||||
async def fake_credentials():
|
||||
return PlainTextResponse(credentials_txt())
|
||||
|
||||
|
||||
@router.get("/passwords.txt")
|
||||
@router.get("/admin_notes.txt")
|
||||
async def fake_passwords():
|
||||
return PlainTextResponse(passwords_txt())
|
||||
|
||||
|
||||
@router.get("/users.json")
|
||||
async def fake_users_json():
|
||||
return JSONResponse(content=None, status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
@router.get("/api_keys.json")
|
||||
async def fake_api_keys():
|
||||
return Response(content=api_keys_json(), status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
@router.get("/config.json")
|
||||
async def fake_config_json():
|
||||
return Response(content=api_response("/api/config"), status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
# Override the generic /users.json to return actual content
|
||||
@router.get("/users.json", include_in_schema=False)
|
||||
async def fake_users_json_content():
|
||||
return Response(content=users_json(), status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
@router.get("/admin")
|
||||
@router.get("/admin/")
|
||||
@router.get("/admin/login")
|
||||
@router.get("/login")
|
||||
async def fake_login():
|
||||
return HTMLResponse(html_templates.login_form())
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
@router.get("/user")
|
||||
@router.get("/database")
|
||||
@router.get("/db")
|
||||
@router.get("/search")
|
||||
async def fake_product_search():
|
||||
return HTMLResponse(html_templates.product_search())
|
||||
|
||||
|
||||
@router.get("/info")
|
||||
@router.get("/input")
|
||||
@router.get("/contact")
|
||||
@router.get("/feedback")
|
||||
@router.get("/comment")
|
||||
async def fake_input_form():
|
||||
return HTMLResponse(html_templates.input_form())
|
||||
|
||||
|
||||
@router.get("/server")
|
||||
async def fake_server_error():
|
||||
error_html, content_type = generate_server_error()
|
||||
return Response(content=error_html, status_code=500, media_type=content_type)
|
||||
|
||||
|
||||
@router.get("/wp-login.php")
|
||||
@router.get("/wp-login")
|
||||
@router.get("/wp-admin")
|
||||
@router.get("/wp-admin/")
|
||||
async def fake_wp_login():
|
||||
return HTMLResponse(html_templates.wp_login())
|
||||
|
||||
|
||||
@router.get("/wp-content/{path:path}")
|
||||
@router.get("/wp-includes/{path:path}")
|
||||
async def fake_wordpress(path: str = ""):
|
||||
return HTMLResponse(html_templates.wordpress())
|
||||
|
||||
|
||||
@router.get("/phpmyadmin")
|
||||
@router.get("/phpmyadmin/{path:path}")
|
||||
@router.get("/phpMyAdmin")
|
||||
@router.get("/phpMyAdmin/{path:path}")
|
||||
@router.get("/pma")
|
||||
@router.get("/pma/")
|
||||
async def fake_phpmyadmin(path: str = ""):
|
||||
return HTMLResponse(html_templates.phpmyadmin())
|
||||
|
||||
|
||||
@router.get("/.env")
|
||||
async def fake_env():
|
||||
return Response(content=api_response("/.env"), status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
@router.get("/backup/")
|
||||
@router.get("/uploads/")
|
||||
@router.get("/private/")
|
||||
@router.get("/config/")
|
||||
@router.get("/database/")
|
||||
async def fake_directory_listing(request: Request):
|
||||
return HTMLResponse(directory_listing(request.url.path))
|
||||
|
||||
|
||||
# --- SQL injection honeypot GET endpoints ---
|
||||
|
||||
@router.get("/api/search")
|
||||
@router.get("/api/sql")
|
||||
@router.get("/api/database")
|
||||
async def sql_endpoint_get(request: Request):
|
||||
client_ip = get_client_ip(request)
|
||||
access_logger = get_access_logger()
|
||||
app_logger = get_app_logger()
|
||||
|
||||
base_path = request.url.path
|
||||
request_query = request.url.query or ""
|
||||
|
||||
error_msg, content_type, status_code = generate_sql_error_response(request_query)
|
||||
|
||||
if error_msg:
|
||||
access_logger.warning(
|
||||
f"[SQL INJECTION DETECTED] {client_ip} - {base_path} - Query: {request_query[:100] if request_query else 'empty'}"
|
||||
)
|
||||
return Response(content=error_msg, status_code=status_code, media_type=content_type)
|
||||
else:
|
||||
access_logger.info(
|
||||
f"[SQL ENDPOINT] {client_ip} - {base_path} - Query: {request_query[:100] if request_query else 'empty'}"
|
||||
)
|
||||
response_data = get_sql_response_with_data(base_path, request_query)
|
||||
return Response(content=response_data, status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
# --- Generic /api/* fake endpoints ---
|
||||
|
||||
@router.get("/api/{path:path}")
|
||||
async def fake_api_catchall(request: Request, path: str):
|
||||
full_path = f"/api/{path}"
|
||||
return Response(content=api_response(full_path), status_code=200, media_type="application/json")
|
||||
|
||||
|
||||
# --- Catch-all GET (trap pages with random links) ---
|
||||
# This MUST be registered last in the router
|
||||
|
||||
@router.get("/{path:path}")
|
||||
async def trap_page(request: Request, path: str):
|
||||
"""Generate trap page with random links. This is the catch-all route."""
|
||||
config = request.app.state.config
|
||||
tracker = request.app.state.tracker
|
||||
app_logger = get_app_logger()
|
||||
access_logger = get_access_logger()
|
||||
|
||||
client_ip = get_client_ip(request)
|
||||
user_agent = request.headers.get("User-Agent", "")
|
||||
full_path = f"/{path}" if path else "/"
|
||||
|
||||
# Check wordpress-like paths
|
||||
if "wordpress" in full_path.lower():
|
||||
return HTMLResponse(html_templates.wordpress())
|
||||
|
||||
# Record access
|
||||
tracker.record_access(
|
||||
client_ip,
|
||||
full_path,
|
||||
user_agent,
|
||||
method="GET",
|
||||
raw_request=build_raw_request(request),
|
||||
)
|
||||
|
||||
if tracker.is_suspicious_user_agent(user_agent):
|
||||
access_logger.warning(
|
||||
f"[SUSPICIOUS] {client_ip} - {user_agent[:50]} - {full_path}"
|
||||
)
|
||||
|
||||
# Random error response
|
||||
if _should_return_error(config):
|
||||
error_code = _get_random_error_code()
|
||||
access_logger.info(
|
||||
f"Returning error {error_code} to {client_ip} - {full_path}"
|
||||
)
|
||||
return Response(status_code=error_code)
|
||||
|
||||
# Response delay
|
||||
await asyncio.sleep(config.delay / 1000.0)
|
||||
|
||||
# Increment page visit counter
|
||||
current_visit_count = tracker.increment_page_visit(client_ip)
|
||||
|
||||
# Generate page
|
||||
page_html = _generate_page(
|
||||
config, tracker, client_ip, full_path, current_visit_count, request.app
|
||||
)
|
||||
|
||||
# Decrement canary counter
|
||||
request.app.state.counter -= 1
|
||||
if request.app.state.counter < 0:
|
||||
request.app.state.counter = config.canary_token_tries
|
||||
|
||||
return HTMLResponse(content=page_html, status_code=200)
|
||||
|
||||
|
||||
def _generate_page(config, tracker, client_ip, seed, page_visit_count, app) -> str:
|
||||
"""Generate a webpage containing random links or canary token."""
|
||||
random.seed(seed)
|
||||
|
||||
ip_category = tracker.get_category_by_ip(client_ip)
|
||||
|
||||
should_apply_crawler_limit = False
|
||||
if config.infinite_pages_for_malicious:
|
||||
if (
|
||||
ip_category == "good_crawler" or ip_category == "regular_user"
|
||||
) and page_visit_count >= config.max_pages_limit:
|
||||
should_apply_crawler_limit = True
|
||||
else:
|
||||
if (
|
||||
ip_category == "good_crawler"
|
||||
or ip_category == "bad_crawler"
|
||||
or ip_category == "attacker"
|
||||
) and page_visit_count >= config.max_pages_limit:
|
||||
should_apply_crawler_limit = True
|
||||
|
||||
if should_apply_crawler_limit:
|
||||
return html_templates.main_page(
|
||||
app.state.counter, "<p>Crawl limit reached.</p>"
|
||||
)
|
||||
|
||||
num_pages = random.randint(*config.links_per_page_range)
|
||||
content = ""
|
||||
|
||||
if app.state.counter <= 0 and config.canary_token_url:
|
||||
content += f"""
|
||||
<div class="link-box canary-token">
|
||||
<a href="{config.canary_token_url}">{config.canary_token_url}</a>
|
||||
</div>
|
||||
"""
|
||||
|
||||
webpages = app.state.webpages
|
||||
if webpages is None:
|
||||
for _ in range(num_pages):
|
||||
address = "".join(
|
||||
[
|
||||
random.choice(config.char_space)
|
||||
for _ in range(random.randint(*config.links_length_range))
|
||||
]
|
||||
)
|
||||
content += f"""
|
||||
<div class="link-box">
|
||||
<a href="{address}">{address}</a>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
for _ in range(num_pages):
|
||||
address = random.choice(webpages)
|
||||
content += f"""
|
||||
<div class="link-box">
|
||||
<a href="{address}">{address}</a>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return html_templates.main_page(app.state.counter, content)
|
||||
307
src/routes/htmx.py
Normal file
307
src/routes/htmx.py
Normal file
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
HTMX fragment endpoints.
|
||||
Server-rendered HTML partials for table pagination, sorting, and IP details.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request, Response, Query
|
||||
|
||||
from dependencies import get_db, get_templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _dashboard_path(request: Request) -> str:
|
||||
config = request.app.state.config
|
||||
return "/" + config.dashboard_secret_path.lstrip("/")
|
||||
|
||||
|
||||
# ── Honeypot Triggers ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/honeypot")
|
||||
async def htmx_honeypot(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_honeypot_paginated(
|
||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/honeypot_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["honeypots"],
|
||||
"pagination": result["pagination"],
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Top IPs ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/top-ips")
|
||||
async def htmx_top_ips(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_top_ips_paginated(
|
||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/top_ips_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["ips"],
|
||||
"pagination": result["pagination"],
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Top Paths ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/top-paths")
|
||||
async def htmx_top_paths(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_top_paths_paginated(
|
||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/top_paths_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["paths"],
|
||||
"pagination": result["pagination"],
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Top User-Agents ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/top-ua")
|
||||
async def htmx_top_ua(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("count"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_top_user_agents_paginated(
|
||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/top_ua_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["user_agents"],
|
||||
"pagination": result["pagination"],
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Attackers ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/attackers")
|
||||
async def htmx_attackers(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("total_requests"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_attackers_paginated(
|
||||
page=max(1, page), page_size=25, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
# Normalize pagination key (DB returns total_attackers, template expects total)
|
||||
pagination = result["pagination"]
|
||||
if "total_attackers" in pagination and "total" not in pagination:
|
||||
pagination["total"] = pagination["total_attackers"]
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/attackers_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["attackers"],
|
||||
"pagination": pagination,
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Credentials ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/credentials")
|
||||
async def htmx_credentials(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("timestamp"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_credentials_paginated(
|
||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/credentials_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": result["credentials"],
|
||||
"pagination": result["pagination"],
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Attack Types ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/attacks")
|
||||
async def htmx_attacks(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
sort_by: str = Query("timestamp"),
|
||||
sort_order: str = Query("desc"),
|
||||
):
|
||||
db = get_db()
|
||||
result = db.get_attack_types_paginated(
|
||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
# Transform attack data for template (join attack_types list, map id to log_id)
|
||||
items = []
|
||||
for attack in result["attacks"]:
|
||||
items.append(
|
||||
{
|
||||
"ip": attack["ip"],
|
||||
"path": attack["path"],
|
||||
"attack_type": ", ".join(attack.get("attack_types", [])),
|
||||
"user_agent": attack.get("user_agent", ""),
|
||||
"timestamp": attack.get("timestamp"),
|
||||
"log_id": attack.get("id"),
|
||||
}
|
||||
)
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/attack_types_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": items,
|
||||
"pagination": result["pagination"],
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── Attack Patterns ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/patterns")
|
||||
async def htmx_patterns(
|
||||
request: Request,
|
||||
page: int = Query(1),
|
||||
):
|
||||
db = get_db()
|
||||
page = max(1, page)
|
||||
page_size = 10
|
||||
|
||||
# Get all attack type stats and paginate manually
|
||||
result = db.get_attack_types_stats(limit=100)
|
||||
all_patterns = [
|
||||
{"pattern": item["type"], "count": item["count"]}
|
||||
for item in result.get("attack_types", [])
|
||||
]
|
||||
|
||||
total = len(all_patterns)
|
||||
total_pages = max(1, (total + page_size - 1) // page_size)
|
||||
offset = (page - 1) * page_size
|
||||
items = all_patterns[offset : offset + page_size]
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/patterns_table.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"items": items,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"total_pages": total_pages,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── IP Detail ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("/htmx/ip-detail/{ip_address:path}")
|
||||
async def htmx_ip_detail(ip_address: str, request: Request):
|
||||
db = get_db()
|
||||
stats = db.get_ip_stats_by_ip(ip_address)
|
||||
|
||||
if not stats:
|
||||
stats = {"ip": ip_address, "total_requests": "N/A"}
|
||||
|
||||
# Transform fields for template compatibility
|
||||
list_on = stats.get("list_on") or {}
|
||||
stats["blocklist_memberships"] = list(list_on.keys()) if list_on else []
|
||||
stats["reverse_dns"] = stats.get("reverse")
|
||||
|
||||
templates = get_templates()
|
||||
return templates.TemplateResponse(
|
||||
"dashboard/partials/ip_detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"dashboard_path": _dashboard_path(request),
|
||||
"stats": stats,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user