diff --git a/app.py b/app.py index 3b6e1ee..42ec4ba 100644 --- a/app.py +++ b/app.py @@ -38,6 +38,7 @@ from utils.constants import ( MAX_BT_DEVICE_AGE_SECONDS, MAX_VESSEL_AGE_SECONDS, MAX_DSC_MESSAGE_AGE_SECONDS, + MAX_DEAUTH_ALERTS_AGE_SECONDS, QUEUE_MAX_SIZE, ) import logging @@ -175,6 +176,11 @@ dsc_lock = threading.Lock() tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE) tscm_lock = threading.Lock() +# Deauth Attack Detection +deauth_detector = None +deauth_detector_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE) +deauth_detector_lock = threading.Lock() + # ============================================ # GLOBAL STATE DICTIONARIES # ============================================ @@ -204,6 +210,9 @@ ais_vessels = DataStore(max_age_seconds=MAX_VESSEL_AGE_SECONDS, name='ais_vessel # DSC (Digital Selective Calling) state - using DataStore for automatic cleanup dsc_messages = DataStore(max_age_seconds=MAX_DSC_MESSAGE_AGE_SECONDS, name='dsc_messages') +# Deauth alerts - using DataStore for automatic cleanup +deauth_alerts = DataStore(max_age_seconds=MAX_DEAUTH_ALERTS_AGE_SECONDS, name='deauth_alerts') + # Satellite state satellite_passes = [] # Predicted satellite passes (not auto-cleaned, calculated) @@ -215,6 +224,7 @@ cleanup_manager.register(bt_beacons) cleanup_manager.register(adsb_aircraft) cleanup_manager.register(ais_vessels) cleanup_manager.register(dsc_messages) +cleanup_manager.register(deauth_alerts) # ============================================ # SDR DEVICE REGISTRY diff --git a/config.py b/config.py index 2250f1d..ba7e0c9 100644 --- a/config.py +++ b/config.py @@ -7,10 +7,17 @@ import os import sys # Application version -VERSION = "2.12.0" +VERSION = "2.12.1" # Changelog - latest release notes (shown on welcome screen) CHANGELOG = [ + { + "version": "2.12.1", + "date": "February 2026", + "highlights": [ + "Bug fixes and improvements", + ] + }, { "version": "2.12.0", "date": "January 2026", @@ -136,27 +143,27 @@ AIRODUMP_HEADER_LINES = _get_env_int('AIRODUMP_HEADER_LINES', 2) BT_SCAN_TIMEOUT = _get_env_int('BT_SCAN_TIMEOUT', 10) BT_UPDATE_INTERVAL = _get_env_float('BT_UPDATE_INTERVAL', 2.0) -# ADS-B settings -ADSB_SBS_PORT = _get_env_int('ADSB_SBS_PORT', 30003) -ADSB_UPDATE_INTERVAL = _get_env_float('ADSB_UPDATE_INTERVAL', 1.0) -ADSB_AUTO_START = _get_env_bool('ADSB_AUTO_START', False) -ADSB_HISTORY_ENABLED = _get_env_bool('ADSB_HISTORY_ENABLED', False) +# ADS-B settings +ADSB_SBS_PORT = _get_env_int('ADSB_SBS_PORT', 30003) +ADSB_UPDATE_INTERVAL = _get_env_float('ADSB_UPDATE_INTERVAL', 1.0) +ADSB_AUTO_START = _get_env_bool('ADSB_AUTO_START', False) +ADSB_HISTORY_ENABLED = _get_env_bool('ADSB_HISTORY_ENABLED', False) ADSB_DB_HOST = _get_env('ADSB_DB_HOST', 'localhost') ADSB_DB_PORT = _get_env_int('ADSB_DB_PORT', 5432) ADSB_DB_NAME = _get_env('ADSB_DB_NAME', 'intercept_adsb') ADSB_DB_USER = _get_env('ADSB_DB_USER', 'intercept') ADSB_DB_PASSWORD = _get_env('ADSB_DB_PASSWORD', 'intercept') -ADSB_HISTORY_BATCH_SIZE = _get_env_int('ADSB_HISTORY_BATCH_SIZE', 500) -ADSB_HISTORY_FLUSH_INTERVAL = _get_env_float('ADSB_HISTORY_FLUSH_INTERVAL', 1.0) -ADSB_HISTORY_QUEUE_SIZE = _get_env_int('ADSB_HISTORY_QUEUE_SIZE', 50000) - -# Observer location settings -SHARED_OBSERVER_LOCATION_ENABLED = _get_env_bool('SHARED_OBSERVER_LOCATION', True) - -# Satellite settings -SATELLITE_UPDATE_INTERVAL = _get_env_int('SATELLITE_UPDATE_INTERVAL', 30) -SATELLITE_TRAJECTORY_POINTS = _get_env_int('SATELLITE_TRAJECTORY_POINTS', 30) -SATELLITE_ORBIT_MINUTES = _get_env_int('SATELLITE_ORBIT_MINUTES', 45) +ADSB_HISTORY_BATCH_SIZE = _get_env_int('ADSB_HISTORY_BATCH_SIZE', 500) +ADSB_HISTORY_FLUSH_INTERVAL = _get_env_float('ADSB_HISTORY_FLUSH_INTERVAL', 1.0) +ADSB_HISTORY_QUEUE_SIZE = _get_env_int('ADSB_HISTORY_QUEUE_SIZE', 50000) + +# Observer location settings +SHARED_OBSERVER_LOCATION_ENABLED = _get_env_bool('SHARED_OBSERVER_LOCATION', True) + +# Satellite settings +SATELLITE_UPDATE_INTERVAL = _get_env_int('SATELLITE_UPDATE_INTERVAL', 30) +SATELLITE_TRAJECTORY_POINTS = _get_env_int('SATELLITE_TRAJECTORY_POINTS', 30) +SATELLITE_ORBIT_MINUTES = _get_env_int('SATELLITE_ORBIT_MINUTES', 45) # Update checking GITHUB_REPO = _get_env('GITHUB_REPO', 'smittix/intercept') diff --git a/pyproject.toml b/pyproject.toml index 7dc672d..6e139ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "intercept" -version = "2.12.0" +version = "2.12.1" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" readme = "README.md" requires-python = ">=3.9" diff --git a/requirements.txt b/requirements.txt index a679ccf..a6d0adf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,9 @@ pyserial>=3.5 # Meshtastic mesh network support (optional - only needed for Meshtastic features) meshtastic>=2.0.0 +# Deauthentication attack detection (optional - for WiFi TSCM) +scapy>=2.4.5 + # QR code generation for Meshtastic channels (optional) qrcode[pil]>=7.4 diff --git a/routes/wifi.py b/routes/wifi.py index 5b833cc..218b978 100644 --- a/routes/wifi.py +++ b/routes/wifi.py @@ -1413,3 +1413,143 @@ def v2_clear_data(): except Exception as e: logger.exception("Error clearing data") return jsonify({'error': str(e)}), 500 + + +# ============================================================================= +# V2 Deauth Detection Endpoints +# ============================================================================= + +@wifi_bp.route('/v2/deauth/status') +def v2_deauth_status(): + """ + Get deauth detection status and recent alerts. + + Returns: + - is_running: Whether deauth detector is active + - interface: Monitor interface being used + - stats: Detection statistics + - recent_alerts: Recent deauth alerts + """ + try: + scanner = get_wifi_scanner() + detector = scanner.deauth_detector + + if detector: + stats = detector.stats + alerts = detector.get_alerts(limit=50) + else: + stats = { + 'is_running': False, + 'interface': None, + 'packets_captured': 0, + 'alerts_generated': 0, + } + alerts = [] + + return jsonify({ + 'is_running': stats.get('is_running', False), + 'interface': stats.get('interface'), + 'started_at': stats.get('started_at'), + 'stats': { + 'packets_captured': stats.get('packets_captured', 0), + 'alerts_generated': stats.get('alerts_generated', 0), + 'active_trackers': stats.get('active_trackers', 0), + }, + 'recent_alerts': alerts, + }) + except Exception as e: + logger.exception("Error getting deauth status") + return jsonify({'error': str(e)}), 500 + + +@wifi_bp.route('/v2/deauth/stream') +def v2_deauth_stream(): + """ + SSE stream for real-time deauth alerts. + + Events: + - deauth_alert: A deauth attack was detected + - deauth_detector_started: Detector started + - deauth_detector_stopped: Detector stopped + - deauth_error: An error occurred + - keepalive: Periodic keepalive + """ + def generate(): + last_keepalive = time.time() + keepalive_interval = SSE_KEEPALIVE_INTERVAL + + while True: + try: + # Try to get from the dedicated deauth queue + msg = app_module.deauth_detector_queue.get(timeout=SSE_QUEUE_TIMEOUT) + last_keepalive = time.time() + yield format_sse(msg) + except queue.Empty: + now = time.time() + if now - last_keepalive >= keepalive_interval: + yield format_sse({'type': 'keepalive'}) + last_keepalive = now + + response = Response(generate(), mimetype='text/event-stream') + response.headers['Cache-Control'] = 'no-cache' + response.headers['X-Accel-Buffering'] = 'no' + response.headers['Connection'] = 'keep-alive' + return response + + +@wifi_bp.route('/v2/deauth/alerts') +def v2_deauth_alerts(): + """ + Get historical deauth alerts. + + Query params: + - limit: Maximum number of alerts to return (default 100) + """ + try: + limit = request.args.get('limit', 100, type=int) + limit = max(1, min(limit, 1000)) # Clamp between 1 and 1000 + + scanner = get_wifi_scanner() + alerts = scanner.get_deauth_alerts(limit=limit) + + # Also include alerts from DataStore that might have been persisted + try: + stored_alerts = list(app_module.deauth_alerts.values()) + # Merge and deduplicate by ID + alert_ids = {a.get('id') for a in alerts} + for alert in stored_alerts: + if alert.get('id') not in alert_ids: + alerts.append(alert) + # Sort by timestamp descending + alerts.sort(key=lambda a: a.get('timestamp', 0), reverse=True) + alerts = alerts[:limit] + except Exception: + pass + + return jsonify({ + 'alerts': alerts, + 'count': len(alerts), + }) + except Exception as e: + logger.exception("Error getting deauth alerts") + return jsonify({'error': str(e)}), 500 + + +@wifi_bp.route('/v2/deauth/clear', methods=['POST']) +def v2_deauth_clear(): + """Clear deauth alert history.""" + try: + scanner = get_wifi_scanner() + scanner.clear_deauth_alerts() + + # Clear the queue + while not app_module.deauth_detector_queue.empty(): + try: + app_module.deauth_detector_queue.get_nowait() + except queue.Empty: + break + + return jsonify({'status': 'cleared'}) + except Exception as e: + logger.exception("Error clearing deauth alerts") + return jsonify({'error': str(e)}), 500 diff --git a/static/css/index.css b/static/css/index.css index 6bc9d3c..62e13e9 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -978,6 +978,18 @@ header h1 { color: var(--accent-cyan); } +/* Donate button - warm amber accent */ +.nav-tool-btn--donate { + text-decoration: none; + color: var(--accent-amber); +} + +.nav-tool-btn--donate:hover { + color: var(--accent-orange); + border-color: var(--accent-amber); + background: rgba(212, 168, 83, 0.1); +} + /* Theme toggle icon states in nav bar */ .nav-tool-btn .icon-sun, .nav-tool-btn .icon-moon { diff --git a/static/css/settings.css b/static/css/settings.css index 6a948ad..c98209e 100644 --- a/static/css/settings.css +++ b/static/css/settings.css @@ -351,6 +351,34 @@ color: var(--accent-cyan, #00d4ff); } +/* Donate Button */ +.donate-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 20px; + background: linear-gradient(135deg, var(--accent-amber, #d4a853) 0%, var(--accent-orange, #f59e0b) 100%); + border: none; + border-radius: 6px; + color: #000; + font-size: 13px; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(212, 168, 83, 0.3); +} + +.donate-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(212, 168, 83, 0.4); + filter: brightness(1.1); +} + +.donate-btn:active { + transform: translateY(0); +} + /* Tile Provider Custom URL */ .custom-url-row { margin-top: 8px; diff --git a/templates/index.html b/templates/index.html index d5bb67f..545b745 100644 --- a/templates/index.html +++ b/templates/index.html @@ -423,6 +423,7 @@ +