diff --git a/app.py b/app.py index c68c3ee..2504e59 100644 --- a/app.py +++ b/app.py @@ -114,6 +114,10 @@ aprs_rtl_process = None aprs_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE) aprs_lock = threading.Lock() +# TSCM (Technical Surveillance Countermeasures) +tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE) +tscm_lock = threading.Lock() + # ============================================ # GLOBAL STATE DICTIONARIES # ============================================ diff --git a/data/tscm_frequencies.py b/data/tscm_frequencies.py new file mode 100644 index 0000000..6789c26 --- /dev/null +++ b/data/tscm_frequencies.py @@ -0,0 +1,436 @@ +""" +TSCM (Technical Surveillance Countermeasures) Frequency Database + +Known surveillance device frequencies, sweep presets, and threat signatures +for counter-surveillance operations. +""" + +from __future__ import annotations + +# ============================================================================= +# Known Surveillance Frequencies (MHz) +# ============================================================================= + +SURVEILLANCE_FREQUENCIES = { + 'wireless_mics': [ + {'start': 49.0, 'end': 50.0, 'name': '49 MHz Wireless Mics', 'risk': 'medium'}, + {'start': 72.0, 'end': 76.0, 'name': 'VHF Low Band Mics', 'risk': 'medium'}, + {'start': 170.0, 'end': 216.0, 'name': 'VHF High Band Wireless', 'risk': 'medium'}, + {'start': 470.0, 'end': 698.0, 'name': 'UHF TV Band Wireless', 'risk': 'medium'}, + {'start': 902.0, 'end': 928.0, 'name': '900 MHz ISM Wireless', 'risk': 'high'}, + {'start': 1880.0, 'end': 1920.0, 'name': 'DECT Wireless', 'risk': 'high'}, + ], + + 'wireless_cameras': [ + {'start': 900.0, 'end': 930.0, 'name': '900 MHz Video TX', 'risk': 'high'}, + {'start': 1200.0, 'end': 1300.0, 'name': '1.2 GHz Video', 'risk': 'high'}, + {'start': 2400.0, 'end': 2483.5, 'name': '2.4 GHz WiFi Cameras', 'risk': 'high'}, + {'start': 5150.0, 'end': 5850.0, 'name': '5.8 GHz Video', 'risk': 'high'}, + ], + + 'gps_trackers': [ + {'start': 824.0, 'end': 849.0, 'name': 'Cellular 850 Uplink', 'risk': 'high'}, + {'start': 869.0, 'end': 894.0, 'name': 'Cellular 850 Downlink', 'risk': 'high'}, + {'start': 1710.0, 'end': 1755.0, 'name': 'AWS Uplink', 'risk': 'high'}, + {'start': 1850.0, 'end': 1910.0, 'name': 'PCS Uplink', 'risk': 'high'}, + {'start': 1930.0, 'end': 1990.0, 'name': 'PCS Downlink', 'risk': 'high'}, + ], + + 'body_worn': [ + {'start': 49.0, 'end': 50.0, 'name': '49 MHz Body Wires', 'risk': 'critical'}, + {'start': 72.0, 'end': 76.0, 'name': 'VHF Low Band Wires', 'risk': 'critical'}, + {'start': 150.0, 'end': 174.0, 'name': 'VHF High Band', 'risk': 'critical'}, + {'start': 380.0, 'end': 400.0, 'name': 'TETRA Band', 'risk': 'high'}, + {'start': 406.0, 'end': 420.0, 'name': 'Federal/Government', 'risk': 'critical'}, + {'start': 450.0, 'end': 470.0, 'name': 'UHF Business Band', 'risk': 'high'}, + ], + + 'common_bugs': [ + {'start': 88.0, 'end': 108.0, 'name': 'FM Broadcast Band Bugs', 'risk': 'low'}, + {'start': 140.0, 'end': 150.0, 'name': 'Low VHF Bugs', 'risk': 'high'}, + {'start': 418.0, 'end': 419.0, 'name': '418 MHz ISM', 'risk': 'medium'}, + {'start': 433.0, 'end': 434.8, 'name': '433 MHz ISM Band', 'risk': 'medium'}, + {'start': 868.0, 'end': 870.0, 'name': '868 MHz ISM (Europe)', 'risk': 'medium'}, + {'start': 315.0, 'end': 316.0, 'name': '315 MHz ISM (US)', 'risk': 'medium'}, + ], + + 'ism_bands': [ + {'start': 26.96, 'end': 27.41, 'name': 'CB Radio / ISM 27 MHz', 'risk': 'low'}, + {'start': 40.66, 'end': 40.70, 'name': 'ISM 40 MHz', 'risk': 'low'}, + {'start': 315.0, 'end': 316.0, 'name': 'ISM 315 MHz (US)', 'risk': 'medium'}, + {'start': 433.05, 'end': 434.79, 'name': 'ISM 433 MHz (EU)', 'risk': 'medium'}, + {'start': 868.0, 'end': 868.6, 'name': 'ISM 868 MHz (EU)', 'risk': 'medium'}, + {'start': 902.0, 'end': 928.0, 'name': 'ISM 915 MHz (US)', 'risk': 'medium'}, + {'start': 2400.0, 'end': 2483.5, 'name': 'ISM 2.4 GHz', 'risk': 'medium'}, + ], +} + + +# ============================================================================= +# Sweep Presets +# ============================================================================= + +SWEEP_PRESETS = { + 'quick': { + 'name': 'Quick Scan', + 'description': 'Fast 2-minute check of most common bug frequencies', + 'duration_seconds': 120, + 'ranges': [ + {'start': 88.0, 'end': 108.0, 'step': 0.1, 'name': 'FM Band'}, + {'start': 433.0, 'end': 435.0, 'step': 0.025, 'name': '433 MHz ISM'}, + {'start': 868.0, 'end': 870.0, 'step': 0.025, 'name': '868 MHz ISM'}, + ], + 'wifi': True, + 'bluetooth': True, + 'rf': True, + }, + + 'standard': { + 'name': 'Standard Sweep', + 'description': 'Comprehensive 5-minute sweep of common surveillance bands', + 'duration_seconds': 300, + 'ranges': [ + {'start': 25.0, 'end': 50.0, 'step': 0.1, 'name': 'HF/Low VHF'}, + {'start': 88.0, 'end': 108.0, 'step': 0.1, 'name': 'FM Band'}, + {'start': 140.0, 'end': 175.0, 'step': 0.025, 'name': 'VHF'}, + {'start': 380.0, 'end': 450.0, 'step': 0.025, 'name': 'UHF Low'}, + {'start': 868.0, 'end': 930.0, 'step': 0.05, 'name': 'ISM 868/915'}, + ], + 'wifi': True, + 'bluetooth': True, + 'rf': True, + }, + + 'full': { + 'name': 'Full Spectrum', + 'description': 'Complete 15-minute spectrum sweep (24 MHz - 1.7 GHz)', + 'duration_seconds': 900, + 'ranges': [ + {'start': 24.0, 'end': 1700.0, 'step': 0.1, 'name': 'Full Spectrum'}, + ], + 'wifi': True, + 'bluetooth': True, + 'rf': True, + }, + + 'wireless_cameras': { + 'name': 'Wireless Cameras', + 'description': 'Focus on video transmission frequencies', + 'duration_seconds': 180, + 'ranges': [ + {'start': 900.0, 'end': 930.0, 'step': 0.1, 'name': '900 MHz Video'}, + {'start': 1200.0, 'end': 1300.0, 'step': 0.5, 'name': '1.2 GHz Video'}, + ], + 'wifi': True, # WiFi cameras + 'bluetooth': False, + 'rf': True, + }, + + 'body_worn': { + 'name': 'Body-Worn Devices', + 'description': 'Detect body wires and covert transmitters', + 'duration_seconds': 240, + 'ranges': [ + {'start': 49.0, 'end': 50.0, 'step': 0.01, 'name': '49 MHz'}, + {'start': 72.0, 'end': 76.0, 'step': 0.01, 'name': 'VHF Low'}, + {'start': 150.0, 'end': 174.0, 'step': 0.0125, 'name': 'VHF High'}, + {'start': 406.0, 'end': 420.0, 'step': 0.0125, 'name': 'Federal'}, + {'start': 450.0, 'end': 470.0, 'step': 0.0125, 'name': 'UHF'}, + ], + 'wifi': False, + 'bluetooth': True, # BLE bugs + 'rf': True, + }, + + 'gps_trackers': { + 'name': 'GPS Trackers', + 'description': 'Detect cellular-based GPS tracking devices', + 'duration_seconds': 180, + 'ranges': [ + {'start': 824.0, 'end': 894.0, 'step': 0.1, 'name': 'Cellular 850'}, + {'start': 1850.0, 'end': 1990.0, 'step': 0.1, 'name': 'PCS Band'}, + ], + 'wifi': False, + 'bluetooth': True, # BLE trackers + 'rf': True, + }, + + 'bluetooth_only': { + 'name': 'Bluetooth/BLE Trackers', + 'description': 'Focus on BLE tracking devices (AirTag, Tile, etc.)', + 'duration_seconds': 60, + 'ranges': [], + 'wifi': False, + 'bluetooth': True, + 'rf': False, + }, + + 'wifi_only': { + 'name': 'WiFi Devices', + 'description': 'Scan for hidden WiFi cameras and access points', + 'duration_seconds': 60, + 'ranges': [], + 'wifi': True, + 'bluetooth': False, + 'rf': False, + }, +} + + +# ============================================================================= +# Known Tracker Signatures +# ============================================================================= + +BLE_TRACKER_SIGNATURES = { + 'apple_airtag': { + 'name': 'Apple AirTag', + 'company_id': 0x004C, + 'patterns': ['findmy', 'airtag'], + 'risk': 'high', + 'description': 'Apple Find My network tracker', + }, + 'tile': { + 'name': 'Tile Tracker', + 'company_id': 0x00ED, + 'patterns': ['tile'], + 'oui_prefixes': ['C4:E7', 'DC:54', 'E6:43'], + 'risk': 'high', + 'description': 'Tile Bluetooth tracker', + }, + 'samsung_smarttag': { + 'name': 'Samsung SmartTag', + 'company_id': 0x0075, + 'patterns': ['smarttag', 'smartthings'], + 'risk': 'high', + 'description': 'Samsung SmartThings tracker', + }, + 'chipolo': { + 'name': 'Chipolo', + 'company_id': 0x0A09, + 'patterns': ['chipolo'], + 'risk': 'high', + 'description': 'Chipolo Bluetooth tracker', + }, + 'generic_beacon': { + 'name': 'Unknown BLE Beacon', + 'company_id': None, + 'patterns': [], + 'risk': 'medium', + 'description': 'Unidentified BLE beacon device', + }, +} + + +# ============================================================================= +# Threat Classification +# ============================================================================= + +THREAT_TYPES = { + 'new_device': { + 'name': 'New Device', + 'description': 'Device not present in baseline', + 'default_severity': 'medium', + }, + 'tracker': { + 'name': 'Tracking Device', + 'description': 'Known BLE tracker detected', + 'default_severity': 'high', + }, + 'unknown_signal': { + 'name': 'Unknown Signal', + 'description': 'Unidentified RF transmission', + 'default_severity': 'medium', + }, + 'burst_transmission': { + 'name': 'Burst Transmission', + 'description': 'Intermittent/store-and-forward signal detected', + 'default_severity': 'high', + }, + 'hidden_camera': { + 'name': 'Potential Hidden Camera', + 'description': 'WiFi camera or video transmitter detected', + 'default_severity': 'critical', + }, + 'gsm_bug': { + 'name': 'GSM/Cellular Bug', + 'description': 'Cellular transmission in non-phone device context', + 'default_severity': 'critical', + }, + 'rogue_ap': { + 'name': 'Rogue Access Point', + 'description': 'Unauthorized WiFi access point', + 'default_severity': 'high', + }, + 'anomaly': { + 'name': 'Signal Anomaly', + 'description': 'Unusual signal pattern or behavior', + 'default_severity': 'low', + }, +} + +SEVERITY_LEVELS = { + 'critical': { + 'level': 4, + 'color': '#ff0000', + 'description': 'Immediate action required - active surveillance likely', + }, + 'high': { + 'level': 3, + 'color': '#ff6600', + 'description': 'Strong indicator of surveillance device', + }, + 'medium': { + 'level': 2, + 'color': '#ffcc00', + 'description': 'Potential threat - requires investigation', + }, + 'low': { + 'level': 1, + 'color': '#00cc00', + 'description': 'Minor anomaly - low probability of threat', + }, +} + + +# ============================================================================= +# WiFi Camera Detection Patterns +# ============================================================================= + +WIFI_CAMERA_PATTERNS = { + 'ssid_patterns': [ + 'cam', 'camera', 'ipcam', 'webcam', 'dvr', 'nvr', + 'hikvision', 'dahua', 'reolink', 'wyze', 'ring', + 'arlo', 'nest', 'blink', 'eufy', 'yi', + ], + 'oui_manufacturers': [ + 'Hikvision', + 'Dahua', + 'Axis Communications', + 'Hanwha Techwin', + 'Vivotek', + 'Ubiquiti', + 'Wyze Labs', + 'Amazon Technologies', # Ring + 'Google', # Nest + ], + 'mac_prefixes': { + 'C0:25:E9': 'TP-Link Camera', + 'A4:DA:22': 'TP-Link Camera', + '78:8C:B5': 'TP-Link Camera', + 'D4:6E:0E': 'TP-Link Camera', + '2C:AA:8E': 'Wyze Camera', + 'AC:CF:85': 'Hikvision', + '54:C4:15': 'Hikvision', + 'C0:56:E3': 'Hikvision', + '3C:EF:8C': 'Dahua', + 'A0:BD:1D': 'Dahua', + 'E4:24:6C': 'Dahua', + }, +} + + +# ============================================================================= +# Utility Functions +# ============================================================================= + +def get_frequency_risk(frequency_mhz: float) -> tuple[str, str]: + """ + Determine the risk level for a given frequency. + + Returns: + Tuple of (risk_level, category_name) + """ + for category, ranges in SURVEILLANCE_FREQUENCIES.items(): + for freq_range in ranges: + if freq_range['start'] <= frequency_mhz <= freq_range['end']: + return freq_range['risk'], freq_range['name'] + + return 'low', 'Unknown Band' + + +def get_sweep_preset(preset_name: str) -> dict | None: + """Get a sweep preset by name.""" + return SWEEP_PRESETS.get(preset_name) + + +def get_all_sweep_presets() -> dict: + """Get all available sweep presets.""" + return { + name: { + 'name': preset['name'], + 'description': preset['description'], + 'duration_seconds': preset['duration_seconds'], + } + for name, preset in SWEEP_PRESETS.items() + } + + +def is_known_tracker(device_name: str | None, manufacturer_data: bytes | None = None) -> dict | None: + """ + Check if a BLE device matches known tracker signatures. + + Returns: + Tracker info dict if match found, None otherwise + """ + if device_name: + name_lower = device_name.lower() + for tracker_id, tracker_info in BLE_TRACKER_SIGNATURES.items(): + for pattern in tracker_info.get('patterns', []): + if pattern in name_lower: + return tracker_info + + if manufacturer_data and len(manufacturer_data) >= 2: + company_id = int.from_bytes(manufacturer_data[:2], 'little') + for tracker_id, tracker_info in BLE_TRACKER_SIGNATURES.items(): + if tracker_info.get('company_id') == company_id: + return tracker_info + + return None + + +def is_potential_camera(ssid: str | None = None, mac: str | None = None, vendor: str | None = None) -> bool: + """Check if a WiFi device might be a hidden camera.""" + if ssid: + ssid_lower = ssid.lower() + for pattern in WIFI_CAMERA_PATTERNS['ssid_patterns']: + if pattern in ssid_lower: + return True + + if mac: + mac_prefix = mac[:8].upper() + if mac_prefix in WIFI_CAMERA_PATTERNS['mac_prefixes']: + return True + + if vendor: + vendor_lower = vendor.lower() + for manufacturer in WIFI_CAMERA_PATTERNS['oui_manufacturers']: + if manufacturer.lower() in vendor_lower: + return True + + return False + + +def get_threat_severity(threat_type: str, context: dict | None = None) -> str: + """ + Determine threat severity based on type and context. + + Args: + threat_type: Type of threat from THREAT_TYPES + context: Optional context dict with signal_strength, etc. + + Returns: + Severity level string + """ + threat_info = THREAT_TYPES.get(threat_type, {}) + base_severity = threat_info.get('default_severity', 'medium') + + if context: + # Upgrade severity based on signal strength (closer = more concerning) + signal = context.get('signal_strength') + if signal and signal > -50: # Very strong signal + if base_severity == 'medium': + return 'high' + elif base_severity == 'high': + return 'critical' + + return base_severity diff --git a/routes/__init__.py b/routes/__init__.py index 9cf04e2..b7960d1 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -14,6 +14,7 @@ def register_blueprints(app): from .settings import settings_bp from .correlation import correlation_bp from .listening_post import listening_post_bp + from .tscm import tscm_bp, init_tscm_state app.register_blueprint(pager_bp) app.register_blueprint(sensor_bp) @@ -27,3 +28,9 @@ def register_blueprints(app): app.register_blueprint(settings_bp) app.register_blueprint(correlation_bp) app.register_blueprint(listening_post_bp) + app.register_blueprint(tscm_bp) + + # Initialize TSCM state with queue and lock from app + import app as app_module + if hasattr(app_module, 'tscm_queue') and hasattr(app_module, 'tscm_lock'): + init_tscm_state(app_module.tscm_queue, app_module.tscm_lock) diff --git a/routes/tscm.py b/routes/tscm.py new file mode 100644 index 0000000..397c850 --- /dev/null +++ b/routes/tscm.py @@ -0,0 +1,639 @@ +""" +TSCM (Technical Surveillance Countermeasures) Routes + +Provides endpoints for counter-surveillance sweeps, baseline management, +threat detection, and reporting. +""" + +from __future__ import annotations + +import json +import logging +import queue +import threading +import time +from datetime import datetime +from typing import Any + +from flask import Blueprint, Response, jsonify, request + +from data.tscm_frequencies import ( + SWEEP_PRESETS, + get_all_sweep_presets, + get_sweep_preset, +) +from utils.database import ( + add_tscm_threat, + acknowledge_tscm_threat, + create_tscm_sweep, + delete_tscm_baseline, + get_active_tscm_baseline, + get_all_tscm_baselines, + get_tscm_baseline, + get_tscm_sweep, + get_tscm_threat_summary, + get_tscm_threats, + set_active_tscm_baseline, + update_tscm_sweep, +) +from utils.tscm.baseline import BaselineComparator, BaselineRecorder +from utils.tscm.detector import ThreatDetector + +logger = logging.getLogger('intercept.tscm') + +tscm_bp = Blueprint('tscm', __name__, url_prefix='/tscm') + +# ============================================================================= +# Global State (will be initialized from app.py) +# ============================================================================= + +# These will be set by app.py +tscm_queue: queue.Queue | None = None +tscm_lock: threading.Lock | None = None + +# Local state +_sweep_thread: threading.Thread | None = None +_sweep_running = False +_current_sweep_id: int | None = None +_baseline_recorder = BaselineRecorder() + + +def init_tscm_state(tscm_q: queue.Queue, lock: threading.Lock) -> None: + """Initialize TSCM state from app.py.""" + global tscm_queue, tscm_lock + tscm_queue = tscm_q + tscm_lock = lock + + +def _emit_event(event_type: str, data: dict) -> None: + """Emit an event to the SSE queue.""" + if tscm_queue: + try: + tscm_queue.put_nowait({ + 'type': event_type, + 'timestamp': datetime.now().isoformat(), + **data + }) + except queue.Full: + logger.warning("TSCM queue full, dropping event") + + +# ============================================================================= +# Sweep Endpoints +# ============================================================================= + +def _check_available_devices(wifi: bool, bt: bool, rf: bool) -> dict: + """Check which scanning devices are available.""" + import shutil + import subprocess + + available = { + 'wifi': False, + 'bluetooth': False, + 'rf': False, + 'wifi_reason': 'Not checked', + 'bt_reason': 'Not checked', + 'rf_reason': 'Not checked', + } + + # Check WiFi + if wifi: + if shutil.which('airodump-ng') or shutil.which('iwlist'): + # Check for wireless interfaces + try: + result = subprocess.run( + ['iwconfig'], + capture_output=True, + text=True, + timeout=5 + ) + if 'no wireless extensions' not in result.stderr.lower() and result.stdout.strip(): + available['wifi'] = True + available['wifi_reason'] = 'Wireless interface detected' + else: + available['wifi_reason'] = 'No wireless interfaces found' + except (subprocess.TimeoutExpired, FileNotFoundError): + available['wifi_reason'] = 'Cannot detect wireless interfaces' + else: + available['wifi_reason'] = 'WiFi tools not installed (aircrack-ng)' + + # Check Bluetooth + if bt: + if shutil.which('bluetoothctl') or shutil.which('hcitool'): + try: + result = subprocess.run( + ['hciconfig'], + capture_output=True, + text=True, + timeout=5 + ) + if 'hci' in result.stdout.lower(): + available['bluetooth'] = True + available['bt_reason'] = 'Bluetooth adapter detected' + else: + available['bt_reason'] = 'No Bluetooth adapters found' + except (subprocess.TimeoutExpired, FileNotFoundError): + # Try bluetoothctl as fallback + try: + result = subprocess.run( + ['bluetoothctl', 'list'], + capture_output=True, + text=True, + timeout=5 + ) + if result.stdout.strip(): + available['bluetooth'] = True + available['bt_reason'] = 'Bluetooth adapter detected' + else: + available['bt_reason'] = 'No Bluetooth adapters found' + except (subprocess.TimeoutExpired, FileNotFoundError): + available['bt_reason'] = 'Cannot detect Bluetooth adapters' + else: + available['bt_reason'] = 'Bluetooth tools not installed (bluez)' + + # Check RF/SDR + if rf: + try: + from utils.sdr import SDRFactory + devices = SDRFactory.detect_devices() + if devices: + available['rf'] = True + available['rf_reason'] = f'{len(devices)} SDR device(s) detected' + else: + available['rf_reason'] = 'No SDR devices found' + except ImportError: + available['rf_reason'] = 'SDR detection unavailable' + + return available + + +@tscm_bp.route('/sweep/start', methods=['POST']) +def start_sweep(): + """Start a TSCM sweep.""" + global _sweep_running, _sweep_thread, _current_sweep_id + + if _sweep_running: + return jsonify({'status': 'error', 'message': 'Sweep already running'}) + + data = request.get_json() or {} + sweep_type = data.get('sweep_type', 'standard') + baseline_id = data.get('baseline_id') + wifi_enabled = data.get('wifi', True) + bt_enabled = data.get('bluetooth', True) + rf_enabled = data.get('rf', True) + + # Check for available devices + devices = _check_available_devices(wifi_enabled, bt_enabled, rf_enabled) + + warnings = [] + if wifi_enabled and not devices['wifi']: + warnings.append(f"WiFi: {devices['wifi_reason']}") + if bt_enabled and not devices['bluetooth']: + warnings.append(f"Bluetooth: {devices['bt_reason']}") + if rf_enabled and not devices['rf']: + warnings.append(f"RF: {devices['rf_reason']}") + + # If no devices available at all, return error + if not any([devices['wifi'], devices['bluetooth'], devices['rf']]): + return jsonify({ + 'status': 'error', + 'message': 'No scanning devices available', + 'details': warnings + }), 400 + + # Create sweep record + _current_sweep_id = create_tscm_sweep( + sweep_type=sweep_type, + baseline_id=baseline_id, + wifi_enabled=wifi_enabled, + bt_enabled=bt_enabled, + rf_enabled=rf_enabled + ) + + _sweep_running = True + + # Start sweep thread + _sweep_thread = threading.Thread( + target=_run_sweep, + args=(sweep_type, baseline_id, wifi_enabled, bt_enabled, rf_enabled), + daemon=True + ) + _sweep_thread.start() + + logger.info(f"Started TSCM sweep: type={sweep_type}, id={_current_sweep_id}") + + return jsonify({ + 'status': 'success', + 'message': 'Sweep started', + 'sweep_id': _current_sweep_id, + 'sweep_type': sweep_type, + 'warnings': warnings if warnings else None, + 'devices': { + 'wifi': devices['wifi'], + 'bluetooth': devices['bluetooth'], + 'rf': devices['rf'] + } + }) + + +@tscm_bp.route('/sweep/stop', methods=['POST']) +def stop_sweep(): + """Stop the current TSCM sweep.""" + global _sweep_running + + if not _sweep_running: + return jsonify({'status': 'error', 'message': 'No sweep running'}) + + _sweep_running = False + + if _current_sweep_id: + update_tscm_sweep(_current_sweep_id, status='aborted', completed=True) + + _emit_event('sweep_stopped', {'reason': 'user_requested'}) + + logger.info("TSCM sweep stopped by user") + + return jsonify({'status': 'success', 'message': 'Sweep stopped'}) + + +@tscm_bp.route('/sweep/status') +def sweep_status(): + """Get current sweep status.""" + status = { + 'running': _sweep_running, + 'sweep_id': _current_sweep_id, + } + + if _current_sweep_id: + sweep = get_tscm_sweep(_current_sweep_id) + if sweep: + status['sweep'] = sweep + + return jsonify(status) + + +@tscm_bp.route('/sweep/stream') +def sweep_stream(): + """SSE stream for real-time sweep updates.""" + def generate(): + while True: + try: + if tscm_queue: + msg = tscm_queue.get(timeout=1) + yield f"data: {json.dumps(msg)}\n\n" + else: + time.sleep(1) + yield f"data: {json.dumps({'type': 'keepalive'})}\n\n" + except queue.Empty: + yield f"data: {json.dumps({'type': 'keepalive'})}\n\n" + + return Response( + generate(), + mimetype='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' + } + ) + + +def _run_sweep( + sweep_type: str, + baseline_id: int | None, + wifi_enabled: bool, + bt_enabled: bool, + rf_enabled: bool +) -> None: + """ + Run the TSCM sweep in a background thread. + + This orchestrates data collection from WiFi, BT, and RF sources, + then analyzes results for threats. + """ + global _sweep_running, _current_sweep_id + + try: + # Get baseline for comparison if specified + baseline = None + if baseline_id: + baseline = get_tscm_baseline(baseline_id) + + # Get sweep preset + preset = get_sweep_preset(sweep_type) or SWEEP_PRESETS.get('standard') + duration = preset.get('duration_seconds', 300) + + _emit_event('sweep_started', { + 'sweep_id': _current_sweep_id, + 'sweep_type': sweep_type, + 'duration': duration, + 'wifi': wifi_enabled, + 'bluetooth': bt_enabled, + 'rf': rf_enabled, + }) + + # Initialize detector + detector = ThreatDetector(baseline) + + # Collect and analyze data + threats_found = 0 + all_wifi = [] + all_bt = [] + all_rf = [] + + start_time = time.time() + + while _sweep_running and (time.time() - start_time) < duration: + # Import app module to access shared data stores + try: + import app as app_module + + # Collect WiFi data + if wifi_enabled and hasattr(app_module, 'wifi_networks'): + wifi_data = list(app_module.wifi_networks.data.values()) + for device in wifi_data: + if device not in all_wifi: + all_wifi.append(device) + threat = detector.analyze_wifi_device(device) + if threat: + _handle_threat(threat) + threats_found += 1 + + # Collect Bluetooth data + if bt_enabled and hasattr(app_module, 'bt_devices'): + bt_data = list(app_module.bt_devices.data.values()) + for device in bt_data: + if device not in all_bt: + all_bt.append(device) + threat = detector.analyze_bt_device(device) + if threat: + _handle_threat(threat) + threats_found += 1 + + except ImportError: + logger.warning("Could not import app module for data collection") + + # Update progress + elapsed = time.time() - start_time + progress = min(100, int((elapsed / duration) * 100)) + + _emit_event('sweep_progress', { + 'progress': progress, + 'elapsed': int(elapsed), + 'duration': duration, + 'wifi_count': len(all_wifi), + 'bt_count': len(all_bt), + 'rf_count': len(all_rf), + 'threats_found': threats_found, + }) + + time.sleep(2) # Update every 2 seconds + + # Complete sweep + if _sweep_running and _current_sweep_id: + update_tscm_sweep( + _current_sweep_id, + status='completed', + results={ + 'wifi_devices': len(all_wifi), + 'bt_devices': len(all_bt), + 'rf_signals': len(all_rf), + }, + threats_found=threats_found, + completed=True + ) + + _emit_event('sweep_completed', { + 'sweep_id': _current_sweep_id, + 'threats_found': threats_found, + 'wifi_count': len(all_wifi), + 'bt_count': len(all_bt), + 'rf_count': len(all_rf), + }) + + except Exception as e: + logger.error(f"Sweep error: {e}") + _emit_event('sweep_error', {'error': str(e)}) + if _current_sweep_id: + update_tscm_sweep(_current_sweep_id, status='error', completed=True) + + finally: + _sweep_running = False + + +def _handle_threat(threat: dict) -> None: + """Handle a detected threat.""" + if not _current_sweep_id: + return + + # Add to database + threat_id = add_tscm_threat( + sweep_id=_current_sweep_id, + threat_type=threat['threat_type'], + severity=threat['severity'], + source=threat['source'], + identifier=threat['identifier'], + name=threat.get('name'), + signal_strength=threat.get('signal_strength'), + frequency=threat.get('frequency'), + details=threat.get('details') + ) + + # Emit event + _emit_event('threat_detected', { + 'threat_id': threat_id, + **threat + }) + + logger.warning( + f"TSCM threat detected: {threat['threat_type']} - " + f"{threat['identifier']} ({threat['severity']})" + ) + + +# ============================================================================= +# Baseline Endpoints +# ============================================================================= + +@tscm_bp.route('/baseline/record', methods=['POST']) +def record_baseline(): + """Start recording a new baseline.""" + data = request.get_json() or {} + name = data.get('name', f'Baseline {datetime.now().strftime("%Y-%m-%d %H:%M")}') + location = data.get('location') + description = data.get('description') + + baseline_id = _baseline_recorder.start_recording(name, location, description) + + return jsonify({ + 'status': 'success', + 'message': 'Baseline recording started', + 'baseline_id': baseline_id + }) + + +@tscm_bp.route('/baseline/stop', methods=['POST']) +def stop_baseline(): + """Stop baseline recording.""" + result = _baseline_recorder.stop_recording() + + if 'error' in result: + return jsonify({'status': 'error', 'message': result['error']}) + + return jsonify({ + 'status': 'success', + 'message': 'Baseline recording complete', + **result + }) + + +@tscm_bp.route('/baseline/status') +def baseline_status(): + """Get baseline recording status.""" + return jsonify(_baseline_recorder.get_recording_status()) + + +@tscm_bp.route('/baselines') +def list_baselines(): + """List all baselines.""" + baselines = get_all_tscm_baselines() + return jsonify({'status': 'success', 'baselines': baselines}) + + +@tscm_bp.route('/baseline/') +def get_baseline(baseline_id: int): + """Get a specific baseline.""" + baseline = get_tscm_baseline(baseline_id) + if not baseline: + return jsonify({'status': 'error', 'message': 'Baseline not found'}), 404 + + return jsonify({'status': 'success', 'baseline': baseline}) + + +@tscm_bp.route('/baseline//activate', methods=['POST']) +def activate_baseline(baseline_id: int): + """Set a baseline as active.""" + success = set_active_tscm_baseline(baseline_id) + if not success: + return jsonify({'status': 'error', 'message': 'Baseline not found'}), 404 + + return jsonify({'status': 'success', 'message': 'Baseline activated'}) + + +@tscm_bp.route('/baseline/', methods=['DELETE']) +def remove_baseline(baseline_id: int): + """Delete a baseline.""" + success = delete_tscm_baseline(baseline_id) + if not success: + return jsonify({'status': 'error', 'message': 'Baseline not found'}), 404 + + return jsonify({'status': 'success', 'message': 'Baseline deleted'}) + + +@tscm_bp.route('/baseline/active') +def get_active_baseline(): + """Get the currently active baseline.""" + baseline = get_active_tscm_baseline() + if not baseline: + return jsonify({'status': 'success', 'baseline': None}) + + return jsonify({'status': 'success', 'baseline': baseline}) + + +# ============================================================================= +# Threat Endpoints +# ============================================================================= + +@tscm_bp.route('/threats') +def list_threats(): + """List threats with optional filters.""" + sweep_id = request.args.get('sweep_id', type=int) + severity = request.args.get('severity') + acknowledged = request.args.get('acknowledged') + limit = request.args.get('limit', 100, type=int) + + ack_filter = None + if acknowledged is not None: + ack_filter = acknowledged.lower() in ('true', '1', 'yes') + + threats = get_tscm_threats( + sweep_id=sweep_id, + severity=severity, + acknowledged=ack_filter, + limit=limit + ) + + return jsonify({'status': 'success', 'threats': threats}) + + +@tscm_bp.route('/threats/summary') +def threat_summary(): + """Get threat count summary by severity.""" + summary = get_tscm_threat_summary() + return jsonify({'status': 'success', 'summary': summary}) + + +@tscm_bp.route('/threats/', methods=['PUT']) +def update_threat(threat_id: int): + """Update a threat (acknowledge, add notes).""" + data = request.get_json() or {} + + if data.get('acknowledge'): + notes = data.get('notes') + success = acknowledge_tscm_threat(threat_id, notes) + if not success: + return jsonify({'status': 'error', 'message': 'Threat not found'}), 404 + + return jsonify({'status': 'success', 'message': 'Threat updated'}) + + +# ============================================================================= +# Preset Endpoints +# ============================================================================= + +@tscm_bp.route('/presets') +def list_presets(): + """List available sweep presets.""" + presets = get_all_sweep_presets() + return jsonify({'status': 'success', 'presets': presets}) + + +@tscm_bp.route('/presets/') +def get_preset(preset_name: str): + """Get details for a specific preset.""" + preset = get_sweep_preset(preset_name) + if not preset: + return jsonify({'status': 'error', 'message': 'Preset not found'}), 404 + + return jsonify({'status': 'success', 'preset': preset}) + + +# ============================================================================= +# Data Feed Endpoints (for adding data during sweeps/baselines) +# ============================================================================= + +@tscm_bp.route('/feed/wifi', methods=['POST']) +def feed_wifi(): + """Feed WiFi device data for baseline recording.""" + data = request.get_json() + if data: + _baseline_recorder.add_wifi_device(data) + return jsonify({'status': 'success'}) + + +@tscm_bp.route('/feed/bluetooth', methods=['POST']) +def feed_bluetooth(): + """Feed Bluetooth device data for baseline recording.""" + data = request.get_json() + if data: + _baseline_recorder.add_bt_device(data) + return jsonify({'status': 'success'}) + + +@tscm_bp.route('/feed/rf', methods=['POST']) +def feed_rf(): + """Feed RF signal data for baseline recording.""" + data = request.get_json() + if data: + _baseline_recorder.add_rf_signal(data) + return jsonify({'status': 'success'}) diff --git a/templates/index.html b/templates/index.html index dc9ed80..ea090fb 100644 --- a/templates/index.html +++ b/templates/index.html @@ -302,6 +302,11 @@ +
+
+ Security + +