Crypto: - Replace broken ChaCha20 (static nonce) with ChaCha20-Poly1305 AEAD - HKDF-SHA256 key derivation from per-device factory NVS master keys - Random 12-byte nonce per message (ESP32 hardware RNG) - crypto_init/encrypt/decrypt API with mbedtls legacy (ESP-IDF v5.3.2) - Custom partition table with factory NVS (fctry at 0x10000) Firmware: - crypto.c full rewrite, messages.c device_id prefix + AEAD encrypt - crypto_init() at boot with esp_restart() on failure - Fix command_t initializations across all modules (sub/help fields) - Clean CMakeLists dependencies for ESP-IDF v5.3.2 C3PO (C2): - Rename tools/c2 + tools/c3po -> tools/C3PO - Per-device CryptoContext with HKDF key derivation - KeyStore (keys.json) for master key management - Transport parses device_id:base64(...) wire format Tools: - New tools/provisioning/provision.py for factory NVS key generation - Updated flasher with mbedtls config for v5.3.2 Docs: - Update all READMEs for new crypto, C3PO paths, provisioning - Update roadmap, architecture diagrams, security sections - Update CONTRIBUTING.md project structure
198 lines
6.6 KiB
Python
198 lines
6.6 KiB
Python
"""Unified Flask web server for ESPILON C2 dashboard."""
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
import threading
|
|
from typing import Optional
|
|
|
|
from flask import Flask
|
|
from werkzeug.serving import make_server
|
|
|
|
from .mlat import MlatEngine
|
|
from .auth import create_auth_decorators
|
|
from .routes import (
|
|
create_pages_blueprint,
|
|
create_devices_blueprint,
|
|
create_cameras_blueprint,
|
|
create_mlat_blueprint,
|
|
create_stats_blueprint,
|
|
)
|
|
|
|
# Make hp_dashboard importable (lives in espilon-honey-pot/tools/)
|
|
_HP_TOOLS_DIR = os.environ.get("HP_DASHBOARD_PATH", os.path.normpath(os.path.join(
|
|
os.path.dirname(__file__), "..", "..", "..", "..", "espilon-honey-pot", "tools"
|
|
)))
|
|
if os.path.isdir(_HP_TOOLS_DIR) and _HP_TOOLS_DIR not in sys.path:
|
|
sys.path.insert(0, _HP_TOOLS_DIR)
|
|
|
|
# Disable Flask/Werkzeug request logging
|
|
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
|
|
|
|
|
class UnifiedWebServer:
|
|
"""
|
|
Unified Flask-based web server for ESPILON C2.
|
|
|
|
Provides:
|
|
- Dashboard: View connected ESP32 devices
|
|
- Cameras: View live camera streams with recording
|
|
- MLAT: Visualize multilateration positioning
|
|
"""
|
|
|
|
def __init__(self,
|
|
host: str = "0.0.0.0",
|
|
port: int = 8000,
|
|
image_dir: str = "static/streams",
|
|
username: str = "admin",
|
|
password: str = "admin",
|
|
secret_key: str = "change_this_for_prod",
|
|
multilat_token: str = "multilat_secret_token",
|
|
device_registry=None,
|
|
mlat_engine: Optional[MlatEngine] = None,
|
|
camera_receiver=None,
|
|
hp_store=None,
|
|
hp_commander=None,
|
|
hp_alerts=None,
|
|
hp_geo=None):
|
|
"""
|
|
Initialize the unified web server.
|
|
|
|
Args:
|
|
host: Host to bind the server
|
|
port: Port for the web server
|
|
image_dir: Directory containing camera frame images
|
|
username: Login username
|
|
password: Login password
|
|
secret_key: Flask session secret key
|
|
multilat_token: Bearer token for MLAT API
|
|
device_registry: DeviceRegistry instance for device listing
|
|
mlat_engine: MlatEngine instance (created if None)
|
|
camera_receiver: UDPReceiver instance for camera control
|
|
hp_store: HpStore instance for honeypot event storage
|
|
hp_commander: HpCommander instance for honeypot command dispatch
|
|
hp_alerts: HpAlertEngine instance for honeypot alert rules
|
|
hp_geo: HpGeoLookup instance for geo-IP enrichment
|
|
"""
|
|
self.host = host
|
|
self.port = port
|
|
self.image_dir = image_dir
|
|
self.username = username
|
|
self.password = password
|
|
self.secret_key = secret_key
|
|
self.multilat_token = multilat_token
|
|
self.device_registry = device_registry
|
|
self.mlat = mlat_engine or MlatEngine()
|
|
self.camera_receiver = camera_receiver
|
|
self.hp_store = hp_store
|
|
self.hp_commander = hp_commander
|
|
self.hp_alerts = hp_alerts
|
|
self.hp_geo = hp_geo
|
|
|
|
# C2 root directory
|
|
self.c2_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# Ensure image directory exists
|
|
full_image_dir = os.path.join(self.c2_root, self.image_dir)
|
|
os.makedirs(full_image_dir, exist_ok=True)
|
|
|
|
self._app = self._create_app()
|
|
self._server = None
|
|
self._thread = None
|
|
|
|
def set_camera_receiver(self, receiver):
|
|
"""Set the camera receiver after initialization."""
|
|
self.camera_receiver = receiver
|
|
|
|
@property
|
|
def is_running(self) -> bool:
|
|
return self._thread is not None and self._thread.is_alive()
|
|
|
|
def _create_app(self) -> Flask:
|
|
"""Create and configure the Flask application."""
|
|
template_dir = os.path.join(self.c2_root, "templates")
|
|
static_dir = os.path.join(self.c2_root, "static")
|
|
|
|
app = Flask(__name__,
|
|
template_folder=template_dir,
|
|
static_folder=static_dir)
|
|
app.secret_key = self.secret_key
|
|
|
|
# Create auth decorators
|
|
require_login, require_api_auth = create_auth_decorators(
|
|
lambda: self.multilat_token
|
|
)
|
|
|
|
# Shared config for blueprints
|
|
base_config = {
|
|
"c2_root": self.c2_root,
|
|
"image_dir": self.image_dir,
|
|
"require_login": require_login,
|
|
"require_api_auth": require_api_auth,
|
|
}
|
|
|
|
# Register blueprints
|
|
app.register_blueprint(create_pages_blueprint({
|
|
**base_config,
|
|
"username": self.username,
|
|
"password": self.password,
|
|
}))
|
|
|
|
app.register_blueprint(create_devices_blueprint({
|
|
**base_config,
|
|
"get_device_registry": lambda: self.device_registry,
|
|
}))
|
|
|
|
app.register_blueprint(create_cameras_blueprint({
|
|
**base_config,
|
|
"get_camera_receiver": lambda: self.camera_receiver,
|
|
}))
|
|
|
|
app.register_blueprint(create_mlat_blueprint({
|
|
**base_config,
|
|
"get_mlat_engine": lambda: self.mlat,
|
|
}))
|
|
|
|
app.register_blueprint(create_stats_blueprint({
|
|
**base_config,
|
|
"get_device_registry": lambda: self.device_registry,
|
|
"get_mlat_engine": lambda: self.mlat,
|
|
}))
|
|
|
|
# Honeypot dashboard (optional — only if hp_store is provided)
|
|
if self.hp_store and self.hp_commander:
|
|
try:
|
|
from hp_dashboard import create_hp_blueprint
|
|
app.register_blueprint(create_hp_blueprint({
|
|
**base_config,
|
|
"hp_store": self.hp_store,
|
|
"hp_commander": self.hp_commander,
|
|
"hp_alerts": self.hp_alerts,
|
|
"hp_geo": self.hp_geo,
|
|
}))
|
|
except ImportError:
|
|
pass # hp_dashboard not available
|
|
|
|
return app
|
|
|
|
def start(self) -> bool:
|
|
"""Start the web server in a background thread."""
|
|
if self.is_running:
|
|
return False
|
|
|
|
self._server = make_server(self.host, self.port, self._app, threaded=True)
|
|
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
|
self._thread.start()
|
|
return True
|
|
|
|
def stop(self):
|
|
"""Stop the web server."""
|
|
if self._server:
|
|
self._server.shutdown()
|
|
self._server = None
|
|
self._thread = None
|
|
|
|
def get_url(self) -> str:
|
|
"""Get the server URL."""
|
|
return f"http://{self.host}:{self.port}"
|