Replace monolithic CLI and web server with route-based Flask API. New routes: api_commands, api_build, api_can, api_monitor, api_ota, api_tunnel. Add honeypot security dashboard with real-time SSE, MITRE ATT&CK mapping, kill chain analysis. New TUI with commander/help modules. Add session management, tunnel proxy core, CAN bus data store. Docker support.
412 lines
17 KiB
Python
412 lines
17 KiB
Python
import os
|
|
import time
|
|
from typing import Optional
|
|
|
|
from utils.display import Display
|
|
from tui.help import HelpManager
|
|
from core.session import Session
|
|
from proto.c2_pb2 import Command
|
|
from streams.udp_receiver import UDPReceiver
|
|
from streams.config import (
|
|
UDP_HOST, UDP_PORT, IMAGE_DIR, MULTILAT_AUTH_TOKEN,
|
|
WEB_HOST, WEB_PORT, DEFAULT_USERNAME, DEFAULT_PASSWORD, FLASK_SECRET_KEY
|
|
)
|
|
from web.server import UnifiedWebServer
|
|
|
|
|
|
class Commander:
|
|
"""Routes text commands to handlers. No UI, no readline."""
|
|
|
|
def __init__(self, session: Session):
|
|
self.session = session
|
|
self.help_manager = HelpManager(session.commands, dev_mode=True)
|
|
|
|
def execute(self, cmd: str):
|
|
"""Execute a command string. Called by TUI on submit."""
|
|
if not cmd:
|
|
return
|
|
parts = cmd.split()
|
|
action = parts[0]
|
|
|
|
dispatch = {
|
|
"help": lambda: self.help_manager.show(parts[1:]),
|
|
"exit": lambda: Display.system_message("Use Ctrl+Q to quit"),
|
|
"clear": lambda: Display.system_message("Use Ctrl+L to clear logs"),
|
|
"list": lambda: self._handle_list(),
|
|
"modules": lambda: self.help_manager.show_modules(),
|
|
"group": lambda: self._handle_group(parts[1:]),
|
|
"send": lambda: self._handle_send(parts),
|
|
"active_commands": lambda: self._handle_active_commands(),
|
|
"web": lambda: self._handle_web(parts[1:]),
|
|
"camera": lambda: self._handle_camera(parts[1:]),
|
|
"can": lambda: self._handle_can(parts[1:]),
|
|
}
|
|
|
|
handler = dispatch.get(action)
|
|
if handler:
|
|
handler()
|
|
else:
|
|
Display.error(f"Unknown command: {action}")
|
|
|
|
# ================= HANDLERS =================
|
|
|
|
def _handle_list(self):
|
|
now = time.time()
|
|
active_devices = self.session.registry.all()
|
|
|
|
if not active_devices:
|
|
Display.system_message("No devices currently connected.")
|
|
return
|
|
|
|
Display.system_message("Connected Devices:")
|
|
Display.system_message(f" {'ID':<18}{'IP Address':<18}{'Status':<18}{'Connected For':<18}{'Last Seen':<18}")
|
|
Display.system_message(" " + "-" * 90)
|
|
|
|
for d in active_devices:
|
|
connected_for = Display.format_duration(now - d.connected_at)
|
|
last_seen_duration = Display.format_duration(now - d.last_seen)
|
|
Display.system_message(f" {d.id:<18}{d.address[0]:<18}{d.status:<18}{connected_for:<18}{last_seen_duration}")
|
|
|
|
def _handle_send(self, parts):
|
|
if len(parts) < 3:
|
|
Display.error("Usage: send <id|all|group> <command> [args...]")
|
|
return
|
|
|
|
target_specifier = parts[1]
|
|
command_parts = parts[2:]
|
|
|
|
devices_to_target = []
|
|
target_description = ""
|
|
|
|
if target_specifier == "all":
|
|
devices_to_target = self.session.registry.all()
|
|
target_description = "all connected devices"
|
|
elif target_specifier == "group":
|
|
if len(command_parts) < 2:
|
|
Display.error("Usage: send group <name> <command> [args...]")
|
|
return
|
|
group_name = command_parts[0]
|
|
command_parts = command_parts[1:]
|
|
group_members_ids = self.session.groups.get(group_name)
|
|
if not group_members_ids:
|
|
Display.error(f"Group '{group_name}' not found or is empty.")
|
|
return
|
|
|
|
active_group_devices = []
|
|
for esp_id in group_members_ids:
|
|
dev = self.session.registry.get(esp_id)
|
|
if dev:
|
|
active_group_devices.append(dev)
|
|
else:
|
|
Display.device_event(esp_id, f"Device in group '{group_name}' is not currently connected.")
|
|
|
|
if not active_group_devices:
|
|
Display.error(f"No active devices found in group '{group_name}'.")
|
|
return
|
|
|
|
devices_to_target = active_group_devices
|
|
target_description = f"group '{group_name}' ({', '.join([d.id for d in devices_to_target])})"
|
|
else:
|
|
dev = self.session.registry.get(target_specifier)
|
|
if dev:
|
|
devices_to_target.append(dev)
|
|
target_description = f"device '{target_specifier}'"
|
|
else:
|
|
Display.error(f"Device '{target_specifier}' not found.")
|
|
return
|
|
|
|
if not devices_to_target:
|
|
Display.error("No target devices resolved for sending command.")
|
|
return
|
|
|
|
cmd_name = command_parts[0]
|
|
argv = command_parts[1:]
|
|
|
|
request_id_base = f"req-{int(time.time())}"
|
|
Display.system_message(f"Sending command '{cmd_name}' to {target_description}...")
|
|
|
|
for i, d in enumerate(devices_to_target):
|
|
cmd = Command()
|
|
cmd.device_id = d.id
|
|
cmd.command_name = cmd_name
|
|
cmd.argv.extend(argv)
|
|
|
|
request_id = f"{request_id_base}-{i}"
|
|
cmd.request_id = request_id
|
|
|
|
Display.command_sent(d.id, cmd_name, request_id)
|
|
self.session.transport.send_command(d.sock, cmd, d.id)
|
|
self.session.active_commands[request_id] = {
|
|
"device_id": d.id,
|
|
"command_name": cmd_name,
|
|
"start_time": time.time(),
|
|
"status": "running",
|
|
"output": []
|
|
}
|
|
|
|
def _handle_group(self, parts):
|
|
if not parts:
|
|
Display.error("Usage: group <add|remove|list|show>")
|
|
return
|
|
|
|
cmd = parts[0]
|
|
|
|
if cmd == "add" and len(parts) >= 3:
|
|
group = parts[1]
|
|
added_devices = []
|
|
for esp_id in parts[2:]:
|
|
if self.session.registry.get(esp_id):
|
|
self.session.groups.add_device(group, esp_id)
|
|
added_devices.append(esp_id)
|
|
else:
|
|
Display.device_event(esp_id, "Device not found, skipping group add.")
|
|
if added_devices:
|
|
Display.system_message(f"Group '{group}' updated. Added: {', '.join(added_devices)}")
|
|
else:
|
|
Display.system_message(f"No valid devices to add to group '{group}'.")
|
|
|
|
elif cmd == "remove" and len(parts) >= 3:
|
|
group = parts[1]
|
|
removed_devices = []
|
|
for esp_id in parts[2:]:
|
|
if esp_id in self.session.groups.get(group):
|
|
self.session.groups.remove_device(group, esp_id)
|
|
removed_devices.append(esp_id)
|
|
else:
|
|
Display.device_event(esp_id, f"Device not in group '{group}', skipping remove.")
|
|
if removed_devices:
|
|
Display.system_message(f"Group '{group}' updated. Removed: {', '.join(removed_devices)}")
|
|
else:
|
|
Display.system_message(f"No specified devices found in group '{group}' to remove.")
|
|
|
|
elif cmd == "list":
|
|
all_groups = self.session.groups.all_groups()
|
|
if not all_groups:
|
|
Display.system_message("No groups defined.")
|
|
return
|
|
Display.system_message("Defined Groups:")
|
|
for g, members in all_groups.items():
|
|
Display.system_message(f" {g}: {', '.join(members) if members else 'No members'}")
|
|
|
|
elif cmd == "show" and len(parts) == 2:
|
|
group_name = parts[1]
|
|
members = self.session.groups.get(group_name)
|
|
if members:
|
|
Display.system_message(f"Members of group '{group_name}': {', '.join(members)}")
|
|
else:
|
|
Display.system_message(f"Group '{group_name}' not found or empty.")
|
|
|
|
else:
|
|
Display.error("Invalid group command usage. See 'help group' for details.")
|
|
|
|
def _handle_active_commands(self):
|
|
if not self.session.active_commands:
|
|
Display.system_message("No commands are currently active.")
|
|
return
|
|
|
|
Display.system_message("Active Commands:")
|
|
Display.system_message(f" {'Request ID':<18}{'Device ID':<18}{'Command':<18}{'Status':<18}{'Elapsed Time':<18}")
|
|
Display.system_message(" " + "-" * 90)
|
|
|
|
now = time.time()
|
|
for req_id, cmd_info in self.session.active_commands.items():
|
|
elapsed_time = Display.format_duration(now - cmd_info["start_time"])
|
|
Display.system_message(
|
|
f" {req_id:<18}{cmd_info['device_id']:<18}"
|
|
f"{cmd_info['command_name']:<18}{cmd_info['status']:<18}{elapsed_time}"
|
|
)
|
|
|
|
def _handle_web(self, parts):
|
|
"""Handle web server commands."""
|
|
if not parts:
|
|
Display.error("Usage: web <start|stop|status>")
|
|
return
|
|
|
|
cmd = parts[0]
|
|
|
|
if cmd == "start":
|
|
if self.session.web_server and self.session.web_server.is_running:
|
|
Display.system_message("Web server is already running.")
|
|
return
|
|
|
|
# Initialize honeypot dashboard components
|
|
try:
|
|
from hp_dashboard import HpStore, HpCommander, HpAlertEngine, HpGeoLookup
|
|
_c3po_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
_data_dir = os.path.join(_c3po_root, "data")
|
|
os.makedirs(_data_dir, exist_ok=True)
|
|
if not self.session.hp_store:
|
|
self.session.hp_geo = HpGeoLookup(
|
|
db_path=os.path.join(_data_dir, "honeypot_geo.db"))
|
|
self.session.hp_store = HpStore(
|
|
db_path=os.path.join(_data_dir, "honeypot_events.db"),
|
|
geo_lookup=self.session.hp_geo)
|
|
if not self.session.hp_alerts:
|
|
self.session.hp_alerts = HpAlertEngine(
|
|
db_path=os.path.join(_data_dir, "honeypot_alerts.db"))
|
|
self.session.hp_alerts.set_store(self.session.hp_store)
|
|
if not self.session.hp_commander:
|
|
self.session.hp_commander = HpCommander(
|
|
get_transport=lambda: self.session.transport,
|
|
get_registry=lambda: self.session.registry,
|
|
)
|
|
self.session.transport.hp_store = self.session.hp_store
|
|
self.session.transport.hp_commander = self.session.hp_commander
|
|
Display.system_message("Honeypot dashboard enabled (alerts + geo active)")
|
|
except ImportError:
|
|
Display.system_message("Honeypot dashboard not available (hp_dashboard not found)")
|
|
|
|
self.session.web_server = UnifiedWebServer(
|
|
host=WEB_HOST,
|
|
port=WEB_PORT,
|
|
image_dir=IMAGE_DIR,
|
|
username=DEFAULT_USERNAME,
|
|
password=DEFAULT_PASSWORD,
|
|
secret_key=FLASK_SECRET_KEY,
|
|
device_registry=self.session.registry,
|
|
transport=self.session.transport,
|
|
session=self.session,
|
|
mlat_engine=self.session.mlat_engine,
|
|
multilat_token=MULTILAT_AUTH_TOKEN,
|
|
camera_receiver=self.session.udp_receiver,
|
|
hp_store=self.session.hp_store,
|
|
hp_commander=self.session.hp_commander,
|
|
hp_alerts=self.session.hp_alerts,
|
|
hp_geo=self.session.hp_geo,
|
|
)
|
|
|
|
if self.session.web_server.start():
|
|
Display.system_message(f"Web server started at {self.session.web_server.get_url()}")
|
|
else:
|
|
Display.error("Web server failed to start")
|
|
|
|
elif cmd == "stop":
|
|
if not self.session.web_server or not self.session.web_server.is_running:
|
|
Display.system_message("Web server is not running.")
|
|
return
|
|
|
|
self.session.web_server.stop()
|
|
Display.system_message("Web server stopped.")
|
|
self.session.web_server = None
|
|
|
|
elif cmd == "status":
|
|
Display.system_message("Web Server Status:")
|
|
if self.session.web_server and self.session.web_server.is_running:
|
|
Display.system_message(f" Status: Running")
|
|
Display.system_message(f" URL: {self.session.web_server.get_url()}")
|
|
else:
|
|
Display.system_message(f" Status: Stopped")
|
|
|
|
Display.system_message("MLAT Engine:")
|
|
state = self.session.mlat_engine.get_state()
|
|
Display.system_message(f" Mode: {state.get('coord_mode', 'gps').upper()}")
|
|
Display.system_message(f" Scanners: {state['scanners_count']}")
|
|
if state['target']:
|
|
pos = state['target']['position']
|
|
if 'lat' in pos:
|
|
Display.system_message(f" Target: ({pos['lat']:.6f}, {pos['lon']:.6f})")
|
|
else:
|
|
Display.system_message(f" Target: ({pos['x']:.2f}m, {pos['y']:.2f}m)")
|
|
else:
|
|
Display.system_message(f" Target: Not calculated")
|
|
|
|
else:
|
|
Display.error("Invalid web command. Use: start, stop, status")
|
|
|
|
def _handle_camera(self, parts):
|
|
"""Handle camera UDP receiver commands."""
|
|
if not parts:
|
|
Display.error("Usage: camera <start|stop|status>")
|
|
return
|
|
|
|
cmd = parts[0]
|
|
|
|
if cmd == "start":
|
|
if self.session.udp_receiver and self.session.udp_receiver.is_running:
|
|
Display.system_message("Camera UDP receiver is already running.")
|
|
return
|
|
|
|
self.session.udp_receiver = UDPReceiver(
|
|
host=UDP_HOST,
|
|
port=UDP_PORT,
|
|
image_dir=IMAGE_DIR,
|
|
device_registry=self.session.registry
|
|
)
|
|
|
|
if self.session.udp_receiver.start():
|
|
Display.system_message(f"Camera UDP receiver started on {UDP_HOST}:{UDP_PORT}")
|
|
if self.session.web_server and self.session.web_server.is_running:
|
|
self.session.web_server.set_camera_receiver(self.session.udp_receiver)
|
|
Display.system_message("Web server updated with camera receiver")
|
|
else:
|
|
Display.error("Camera UDP receiver failed to start")
|
|
|
|
elif cmd == "stop":
|
|
if not self.session.udp_receiver or not self.session.udp_receiver.is_running:
|
|
Display.system_message("Camera UDP receiver is not running.")
|
|
return
|
|
|
|
self.session.udp_receiver.stop()
|
|
Display.system_message("Camera UDP receiver stopped.")
|
|
self.session.udp_receiver = None
|
|
if self.session.web_server and self.session.web_server.is_running:
|
|
self.session.web_server.set_camera_receiver(None)
|
|
|
|
elif cmd == "status":
|
|
Display.system_message("Camera UDP Receiver Status:")
|
|
if self.session.udp_receiver and self.session.udp_receiver.is_running:
|
|
stats = self.session.udp_receiver.get_stats()
|
|
Display.system_message(f" Status: Running on {UDP_HOST}:{UDP_PORT}")
|
|
Display.system_message(f" Packets received: {stats['packets_received']}")
|
|
Display.system_message(f" Frames decoded: {stats['frames_received']}")
|
|
Display.system_message(f" Decode errors: {stats['decode_errors']}")
|
|
Display.system_message(f" Invalid tokens: {stats['invalid_tokens']}")
|
|
Display.system_message(f" Active cameras: {stats['active_cameras']}")
|
|
else:
|
|
Display.system_message(f" Status: Stopped")
|
|
|
|
else:
|
|
Display.error("Invalid camera command. Use: start, stop, status")
|
|
|
|
def _handle_can(self, parts):
|
|
"""Handle CAN bus commands — show local CAN store stats or frames."""
|
|
if not parts:
|
|
Display.error("Usage: can <stats|frames|clear> [device_id]")
|
|
return
|
|
|
|
cmd = parts[0]
|
|
|
|
if cmd == "stats":
|
|
device_id = parts[1] if len(parts) > 1 else None
|
|
stats = self.session.can_store.get_stats(device_id=device_id)
|
|
Display.system_message("CAN Bus Statistics:")
|
|
Display.system_message(f" Total received: {stats['total_received']}")
|
|
Display.system_message(f" Stored: {stats['total_stored']}")
|
|
Display.system_message(f" Unique CAN IDs: {stats['unique_can_ids']}")
|
|
if stats['can_ids']:
|
|
Display.system_message(f" IDs: {', '.join(stats['can_ids'][:20])}")
|
|
|
|
elif cmd == "frames":
|
|
device_id = parts[1] if len(parts) > 1 else None
|
|
limit = int(parts[2]) if len(parts) > 2 else 20
|
|
frames = self.session.can_store.get_frames(device_id=device_id, limit=limit)
|
|
if not frames:
|
|
Display.system_message("No CAN frames stored.")
|
|
return
|
|
Display.system_message(f"CAN Frames (last {len(frames)}):")
|
|
Display.system_message(f" {'Device':<12}{'CAN ID':<10}{'DLC':<6}{'Data':<20}{'Timestamp'}")
|
|
Display.system_message(" " + "-" * 70)
|
|
for f in frames:
|
|
Display.system_message(
|
|
f" {f['device_id']:<12}{f['can_id']:<10}{f['dlc']:<6}"
|
|
f"{f['data']:<20}{f['timestamp_ms']}"
|
|
)
|
|
|
|
elif cmd == "clear":
|
|
self.session.can_store.frames.clear()
|
|
self.session.can_store.total_count = 0
|
|
Display.system_message("CAN frame store cleared.")
|
|
|
|
else:
|
|
Display.error("Invalid CAN command. Use: stats, frames, clear")
|