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