Files
intercept/routes/tscm.py
Smittix f36e528086 Add TSCM counter-surveillance mode (Phase 1)
Features:
- New TSCM mode under Security navigation group
- Sweep presets: Quick, Standard, Full, Wireless Cameras, Body-Worn, GPS Trackers
- Device detection with warnings when WiFi/BT/SDR unavailable
- Baseline recording to capture environment "fingerprint"
- Threat detection for known trackers (AirTag, Tile, SmartTag, Chipolo)
- WiFi camera pattern detection
- Real-time SSE streaming for sweep progress
- Futuristic circular scanner progress visualization
- Unified threat dashboard with severity classification

New files:
- routes/tscm.py - TSCM Blueprint with REST API endpoints
- data/tscm_frequencies.py - Surveillance frequency database
- utils/tscm/baseline.py - BaselineRecorder and BaselineComparator
- utils/tscm/detector.py - ThreatDetector for WiFi, BT, RF analysis

Database:
- tscm_baselines, tscm_sweeps, tscm_threats, tscm_schedules tables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:35:13 +00:00

640 lines
20 KiB
Python

"""
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/<int:baseline_id>')
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/<int:baseline_id>/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/<int:baseline_id>', 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/<int:threat_id>', 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/<preset_name>')
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'})