diff --git a/app.py b/app.py index 6bcb4e1..e60d03c 100644 --- a/app.py +++ b/app.py @@ -676,6 +676,7 @@ def kill_all() -> Response: global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process global gsm_spy_livemon_process, gsm_spy_monitor_process + global gsm_spy_scanner_running, gsm_spy_active_device, gsm_spy_selected_arfcn, gsm_spy_region # Import adsb and ais modules to reset their state from routes import adsb as adsb_module @@ -754,6 +755,11 @@ def kill_all() -> Response: # Reset GSM Spy state with gsm_spy_lock: + gsm_spy_scanner_running = False + gsm_spy_active_device = None + gsm_spy_selected_arfcn = None + gsm_spy_region = 'Americas' + if gsm_spy_livemon_process: try: if safe_terminate(gsm_spy_livemon_process): diff --git a/routes/gsm_spy.py b/routes/gsm_spy.py index 57f581f..2166e77 100644 --- a/routes/gsm_spy.py +++ b/routes/gsm_spy.py @@ -463,7 +463,7 @@ def start_monitor(): @gsm_spy_bp.route('/stop', methods=['POST']) def stop_scanner(): """Stop GSM scanner and monitor.""" - global gsm_connected + global gsm_connected, gsm_towers_found, gsm_devices_tracked with app_module.gsm_spy_lock: killed = [] @@ -492,6 +492,8 @@ def stop_scanner(): app_module.gsm_spy_active_device = None app_module.gsm_spy_selected_arfcn = None gsm_connected = False + gsm_towers_found = 0 + gsm_devices_tracked = 0 return jsonify({'status': 'stopped', 'killed': killed}) @@ -1317,13 +1319,7 @@ def scanner_thread(cmd, device_index): # Clean up process with timeout if process.poll() is None: logger.info("Terminating scanner process") - process.terminate() - try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - logger.warning("Process didn't terminate, killing") - process.kill() - process.wait() + safe_terminate(process, timeout=5) else: process.wait() # Reap zombie @@ -1332,14 +1328,7 @@ def scanner_thread(cmd, device_index): except Exception as e: logger.error(f"Scanner scan error: {e}", exc_info=True) if process and process.poll() is None: - try: - process.terminate() - process.wait(timeout=2) - except Exception: - try: - process.kill() - except Exception: - pass + safe_terminate(process) # Check if should continue if not app_module.gsm_spy_scanner_running: @@ -1358,25 +1347,13 @@ def scanner_thread(cmd, device_index): finally: # Always cleanup if process and process.poll() is None: - try: - process.terminate() - process.wait(timeout=5) - except Exception: - try: - process.kill() - process.wait() - except Exception: - pass - - # Unregister process from cleanup list - if process: - unregister_process(process) + safe_terminate(process, timeout=5) logger.info("Scanner thread terminated") # Reset global state with app_module.gsm_spy_lock: - app_module.gsm_spy_scanner_running = None + app_module.gsm_spy_scanner_running = False if app_module.gsm_spy_active_device is not None: from app import release_sdr_device release_sdr_device(app_module.gsm_spy_active_device) diff --git a/templates/gsm_spy_dashboard.html b/templates/gsm_spy_dashboard.html index 18dd0a0..e2e6776 100644 --- a/templates/gsm_spy_dashboard.html +++ b/templates/gsm_spy_dashboard.html @@ -2160,14 +2160,14 @@ const velocity_kmh = (item.estimated_velocity * 3.6).toFixed(2); html += `
-
${item.device_id}
+
${escapeHtml(item.device_id)}
Velocity: - ${velocity_kmh} km/h + ${escapeHtml(velocity_kmh)} km/h
TA Change: - ${item.prev_ta} → ${item.curr_ta} + ${escapeHtml(String(item.prev_ta))} → ${escapeHtml(String(item.curr_ta))}
${new Date(item.timestamp).toLocaleString()}
@@ -2201,18 +2201,18 @@ item.density_level === 'medium' ? 'var(--accent-yellow)' : 'var(--accent-green)'; html += `
-
Cell ${item.cid}
+
Cell ${escapeHtml(String(item.cid))}
Unique Devices: - ${item.unique_devices} + ${escapeHtml(String(item.unique_devices))}
Total Pings: - ${item.total_pings} + ${escapeHtml(String(item.total_pings))}
Density: - ${item.density_level} + ${escapeHtml(item.density_level)}
`; @@ -2241,25 +2241,25 @@ const data = await response.json(); if (data.error) { - contentDiv.innerHTML = `
${data.error}
`; + contentDiv.innerHTML = `
${escapeHtml(data.error)}
`; } else if (data.regular_locations && data.regular_locations.length > 0) { let html = `
- ${data.total_observations} total observations + ${escapeHtml(String(data.total_observations))} total observations
Regular Locations:
`; data.regular_locations.forEach(loc => { html += `
-
Cell ${loc.cid}
+
Cell ${escapeHtml(String(loc.cid))}
Typical Time: - ${loc.typical_time} + ${escapeHtml(loc.typical_time)}
Frequency: - ${loc.frequency} times + ${escapeHtml(String(loc.frequency))} times
`; @@ -2290,17 +2290,17 @@ const data = await response.json(); if (data.error) { - contentDiv.innerHTML = `
${data.error}
`; + contentDiv.innerHTML = `
${escapeHtml(data.error)}
`; } else { const statusColor = data.status === 'suspicious' ? 'var(--accent-red)' : 'var(--accent-green)'; let html = `
Status: - ${data.status} + ${escapeHtml(data.status)}
Neighbor Count: - ${data.neighbor_count} + ${escapeHtml(String(data.neighbor_count))}
`; @@ -2309,8 +2309,8 @@ data.issues.forEach(issue => { html += `
-
${issue.type}
-
${issue.message}
+
${escapeHtml(issue.type)}
+
${escapeHtml(issue.message)}
`; }); @@ -2342,15 +2342,15 @@ const data = await response.json(); if (data.error) { - contentDiv.innerHTML = `
${data.error}
`; + contentDiv.innerHTML = `
${escapeHtml(data.error)}
`; } else { let html = `
- Last ${data.time_window_minutes} minutes + Last ${escapeHtml(String(data.time_window_minutes))} minutes
Active Devices: - ${data.active_devices} + ${escapeHtml(String(data.active_devices))}
`; @@ -2361,16 +2361,16 @@ corr.activity_level === 'medium' ? 'var(--accent-yellow)' : 'var(--accent-green)'; html += `
-
${corr.device_id}
+
${escapeHtml(corr.device_id)}
Burst Count: - ${corr.burst_count} + ${escapeHtml(String(corr.burst_count))}
Activity: - ${corr.activity_level} + ${escapeHtml(corr.activity_level)}
-
TA: ${corr.ta_value}
+
TA: ${escapeHtml(String(corr.ta_value))}
`; }); diff --git a/utils/database.py b/utils/database.py index bedb369..00ab054 100644 --- a/utils/database.py +++ b/utils/database.py @@ -102,6 +102,52 @@ def init_db() -> None: ) ''') + # Alert rules + conn.execute(''' + CREATE TABLE IF NOT EXISTS alert_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + mode TEXT, + event_type TEXT, + match TEXT, + severity TEXT DEFAULT 'medium', + enabled BOOLEAN DEFAULT 1, + notify TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Alert events + conn.execute(''' + CREATE TABLE IF NOT EXISTS alert_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rule_id INTEGER, + mode TEXT, + event_type TEXT, + severity TEXT DEFAULT 'medium', + title TEXT, + message TEXT, + payload TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (rule_id) REFERENCES alert_rules(id) ON DELETE SET NULL + ) + ''') + + # Session recordings + conn.execute(''' + CREATE TABLE IF NOT EXISTS recording_sessions ( + id TEXT PRIMARY KEY, + mode TEXT NOT NULL, + label TEXT, + started_at TIMESTAMP NOT NULL, + stopped_at TIMESTAMP, + file_path TEXT NOT NULL, + event_count INTEGER DEFAULT 0, + size_bytes INTEGER DEFAULT 0, + metadata TEXT + ) + ''') + # Users table for authentication conn.execute(''' CREATE TABLE IF NOT EXISTS users ( @@ -139,6 +185,7 @@ def init_db() -> None: description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, wifi_networks TEXT, + wifi_clients TEXT, bt_devices TEXT, rf_frequencies TEXT, gps_coords TEXT, @@ -146,6 +193,14 @@ def init_db() -> None: ) ''') + # Ensure new columns exist for older databases + try: + columns = {row['name'] for row in conn.execute("PRAGMA table_info(tscm_baselines)")} + if 'wifi_clients' not in columns: + conn.execute('ALTER TABLE tscm_baselines ADD COLUMN wifi_clients TEXT') + except Exception as e: + logger.debug(f"Schema update skipped for tscm_baselines: {e}") + # TSCM Sweeps - Individual sweep sessions conn.execute(''' CREATE TABLE IF NOT EXISTS tscm_sweeps ( @@ -818,6 +873,7 @@ def create_tscm_baseline( location: str | None = None, description: str | None = None, wifi_networks: list | None = None, + wifi_clients: list | None = None, bt_devices: list | None = None, rf_frequencies: list | None = None, gps_coords: dict | None = None @@ -831,13 +887,14 @@ def create_tscm_baseline( with get_db() as conn: cursor = conn.execute(''' INSERT INTO tscm_baselines - (name, location, description, wifi_networks, bt_devices, rf_frequencies, gps_coords) - VALUES (?, ?, ?, ?, ?, ?, ?) + (name, location, description, wifi_networks, wifi_clients, bt_devices, rf_frequencies, gps_coords) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', ( name, location, description, json.dumps(wifi_networks) if wifi_networks else None, + json.dumps(wifi_clients) if wifi_clients else None, json.dumps(bt_devices) if bt_devices else None, json.dumps(rf_frequencies) if rf_frequencies else None, json.dumps(gps_coords) if gps_coords else None