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>
This commit is contained in:
Smittix
2026-01-14 00:13:05 +00:00
parent 52ce930c31
commit f36e528086
10 changed files with 3112 additions and 5 deletions

4
app.py
View File

@@ -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
# ============================================

436
data/tscm_frequencies.py Normal file
View File

@@ -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

View File

@@ -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)

639
routes/tscm.py Normal file
View File

@@ -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/<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'})

View File

@@ -302,6 +302,11 @@
<button class="mode-nav-btn" onclick="switchMode('wifi')"><span class="nav-icon">📶</span><span class="nav-label">WiFi</span></button>
<button class="mode-nav-btn" onclick="switchMode('bluetooth')"><span class="nav-icon">🔵</span><span class="nav-label">Bluetooth</span></button>
</div>
<div class="mode-nav-divider"></div>
<div class="mode-nav-group">
<span class="mode-nav-label">Security</span>
<button class="mode-nav-btn" onclick="switchMode('tscm')"><span class="nav-icon">🔍</span><span class="nav-label">TSCM</span></button>
</div>
<div class="mode-nav-actions">
<a href="/adsb/dashboard" target="_blank" class="nav-action-btn" id="adsbDashboardBtn" style="display: none;">
<span class="nav-icon">🖥️</span><span class="nav-label">Full Dashboard</span>
@@ -1009,6 +1014,104 @@
</div>
<!-- TSCM MODE (Counter-Surveillance) -->
<div id="tscmMode" class="mode-content">
<div class="section">
<h3>TSCM Sweep</h3>
<div class="form-group">
<label>Sweep Type</label>
<select id="tscmSweepType">
<option value="quick">Quick Scan (2 min)</option>
<option value="standard" selected>Standard (5 min)</option>
<option value="full">Full Sweep (15 min)</option>
<option value="wireless_cameras">Wireless Cameras</option>
<option value="body_worn">Body-Worn Devices</option>
<option value="gps_trackers">GPS Trackers</option>
</select>
</div>
<div class="form-group">
<label>Compare Against</label>
<select id="tscmBaselineSelect">
<option value="">No Baseline</option>
</select>
</div>
</div>
<div class="section">
<h3>Scan Sources</h3>
<div class="checkbox-group">
<label>
<input type="checkbox" id="tscmWifiEnabled" checked>
WiFi Networks
</label>
<label>
<input type="checkbox" id="tscmBtEnabled" checked>
Bluetooth Devices
</label>
<label>
<input type="checkbox" id="tscmRfEnabled" checked>
RF Signals
</label>
</div>
</div>
<div class="section">
<h3>Baseline Management</h3>
<div class="form-group">
<input type="text" id="tscmBaselineName" placeholder="Baseline name...">
</div>
<button class="preset-btn" id="tscmRecordBaselineBtn" onclick="tscmRecordBaseline()" style="width: 100%;">
Record New Baseline
</button>
<button class="preset-btn" id="tscmStopBaselineBtn" onclick="tscmStopBaseline()" style="width: 100%; display: none;">
Stop Recording
</button>
<div id="tscmBaselineStatus" style="margin-top: 8px; font-size: 11px; color: var(--text-muted);"></div>
</div>
<div class="section">
<h3>Threat Summary</h3>
<div id="tscmThreatSummary" style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px;">
<div class="threat-card critical"><span class="count">0</span><span class="label">Critical</span></div>
<div class="threat-card high"><span class="count">0</span><span class="label">High</span></div>
<div class="threat-card medium"><span class="count">0</span><span class="label">Medium</span></div>
<div class="threat-card low"><span class="count">0</span><span class="label">Low</span></div>
</div>
</div>
<button class="run-btn" id="startTscmBtn" onclick="startTscmSweep()">
Start Sweep
</button>
<button class="stop-btn" id="stopTscmBtn" onclick="stopTscmSweep()" style="display: none;">
Stop Sweep
</button>
<!-- Futuristic Scanner Progress -->
<div id="tscmProgress" class="tscm-scanner-progress" style="display: none;">
<div class="scanner-ring">
<svg viewBox="0 0 100 100">
<circle class="scanner-track" cx="50" cy="50" r="45" />
<circle class="scanner-progress" id="tscmScannerCircle" cx="50" cy="50" r="45" />
<line class="scanner-sweep" x1="50" y1="50" x2="50" y2="8" />
</svg>
<div class="scanner-center">
<span class="scanner-percent" id="tscmProgressPercent">0%</span>
</div>
</div>
<div class="scanner-info">
<div class="scanner-status" id="tscmProgressLabel">INITIALIZING</div>
<div class="scanner-devices">
<span class="device-indicator" id="tscmWifiIndicator" title="WiFi">📶</span>
<span class="device-indicator" id="tscmBtIndicator" title="Bluetooth">🔵</span>
<span class="device-indicator" id="tscmRfIndicator" title="RF/SDR">📡</span>
</div>
</div>
</div>
<!-- Device Warnings -->
<div id="tscmDeviceWarnings" style="display: none; margin-top: 8px; padding: 8px; background: rgba(255,153,51,0.1); border: 1px solid rgba(255,153,51,0.3); border-radius: 4px;"></div>
</div>
<button class="preset-btn" onclick="killAll()" style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
Kill All Processes
</button>
@@ -1717,6 +1820,280 @@
}
</style>
<!-- TSCM Styles -->
<style>
/* TSCM Threat Cards */
.threat-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
border-radius: 6px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
}
.threat-card .count {
font-size: 18px;
font-weight: bold;
line-height: 1;
}
.threat-card .label {
font-size: 9px;
text-transform: uppercase;
opacity: 0.7;
margin-top: 2px;
}
.threat-card.critical { border-color: #ff3366; color: #ff3366; }
.threat-card.critical.active { background: rgba(255,51,102,0.2); }
.threat-card.high { border-color: #ff9933; color: #ff9933; }
.threat-card.high.active { background: rgba(255,153,51,0.2); }
.threat-card.medium { border-color: #ffcc00; color: #ffcc00; }
.threat-card.medium.active { background: rgba(255,204,0,0.2); }
.threat-card.low { border-color: #00ff88; color: #00ff88; }
.threat-card.low.active { background: rgba(0,255,136,0.2); }
/* TSCM Dashboard */
.tscm-dashboard {
display: flex;
flex-direction: column;
gap: 16px;
height: 100%;
}
.tscm-threat-banner {
display: flex;
gap: 12px;
padding: 12px;
background: rgba(0,0,0,0.3);
border-radius: 8px;
}
.tscm-threat-banner .threat-card {
flex: 1;
padding: 12px;
}
.tscm-threat-banner .threat-card .count {
font-size: 24px;
}
.tscm-main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
flex: 1;
min-height: 0;
}
.tscm-panel {
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tscm-panel-header {
padding: 10px 12px;
background: rgba(0,0,0,0.3);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.tscm-panel-header .badge {
background: var(--primary-color);
color: #fff;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: normal;
}
.tscm-panel-content {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.tscm-device-item {
padding: 8px 10px;
border-radius: 4px;
margin-bottom: 6px;
background: rgba(0,0,0,0.2);
border-left: 3px solid var(--border-color);
cursor: pointer;
transition: background 0.2s;
}
.tscm-device-item:hover {
background: rgba(74,158,255,0.1);
}
.tscm-device-item.new {
border-left-color: #ff9933;
animation: pulse-glow 2s infinite;
}
.tscm-device-item.threat {
border-left-color: #ff3366;
}
.tscm-device-item.baseline {
border-left-color: #00ff88;
}
.tscm-device-name {
font-weight: 600;
font-size: 12px;
margin-bottom: 2px;
}
.tscm-device-meta {
font-size: 10px;
color: var(--text-muted);
display: flex;
gap: 8px;
}
.tscm-threat-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.tscm-threat-item {
padding: 10px 12px;
border-radius: 6px;
background: rgba(0,0,0,0.2);
border: 1px solid;
}
.tscm-threat-item.critical { border-color: #ff3366; background: rgba(255,51,102,0.1); }
.tscm-threat-item.high { border-color: #ff9933; background: rgba(255,153,51,0.1); }
.tscm-threat-item.medium { border-color: #ffcc00; background: rgba(255,204,0,0.1); }
.tscm-threat-item.low { border-color: #00ff88; background: rgba(0,255,136,0.1); }
.tscm-threat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.tscm-threat-type {
font-weight: 600;
font-size: 12px;
}
.tscm-threat-severity {
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
text-transform: uppercase;
}
.tscm-threat-item.critical .tscm-threat-severity { background: #ff3366; color: #fff; }
.tscm-threat-item.high .tscm-threat-severity { background: #ff9933; color: #000; }
.tscm-threat-item.medium .tscm-threat-severity { background: #ffcc00; color: #000; }
.tscm-threat-item.low .tscm-threat-severity { background: #00ff88; color: #000; }
.tscm-threat-details {
font-size: 11px;
color: var(--text-muted);
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 5px rgba(255,153,51,0.3); }
50% { box-shadow: 0 0 15px rgba(255,153,51,0.6); }
}
.tscm-empty {
text-align: center;
padding: 30px;
color: var(--text-muted);
font-size: 12px;
}
/* Futuristic Scanner Progress */
.tscm-scanner-progress {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
margin-top: 10px;
background: rgba(0,0,0,0.4);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.scanner-ring {
position: relative;
width: 70px;
height: 70px;
flex-shrink: 0;
}
.scanner-ring svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.scanner-track {
fill: none;
stroke: rgba(74,158,255,0.1);
stroke-width: 4;
}
.scanner-progress {
fill: none;
stroke: var(--accent-cyan);
stroke-width: 4;
stroke-linecap: round;
stroke-dasharray: 283;
stroke-dashoffset: 283;
transition: stroke-dashoffset 0.3s ease;
filter: drop-shadow(0 0 6px var(--accent-cyan));
}
.scanner-sweep {
stroke: var(--accent-cyan);
stroke-width: 2;
opacity: 0.8;
transform-origin: 50px 50px;
animation: sweep-rotate 2s linear infinite;
filter: drop-shadow(0 0 4px var(--accent-cyan));
}
@keyframes sweep-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.scanner-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.scanner-percent {
font-size: 14px;
font-weight: bold;
color: var(--accent-cyan);
text-shadow: 0 0 10px var(--accent-cyan);
}
.scanner-info {
flex: 1;
min-width: 0;
}
.scanner-status {
font-size: 10px;
font-weight: 600;
letter-spacing: 2px;
color: var(--accent-cyan);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.scanner-devices {
display: flex;
gap: 8px;
}
.device-indicator {
font-size: 14px;
opacity: 0.3;
transition: opacity 0.3s, transform 0.3s;
}
.device-indicator.active {
opacity: 1;
animation: device-pulse 1.5s ease-in-out infinite;
}
.device-indicator.inactive {
opacity: 0.2;
filter: grayscale(1);
}
@keyframes device-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
</style>
<!-- Satellite Dashboard (Embedded) -->
<div id="satelliteVisuals" class="satellite-dashboard-embed" style="display: none;">
<iframe
@@ -1728,6 +2105,65 @@
</iframe>
</div>
<!-- TSCM Dashboard -->
<div id="tscmVisuals" class="tscm-dashboard" style="display: none; padding: 16px;">
<!-- Threat Summary Banner -->
<div class="tscm-threat-banner">
<div class="threat-card critical" id="tscmCriticalCard">
<span class="count" id="tscmCriticalCount">0</span>
<span class="label">Critical</span>
</div>
<div class="threat-card high" id="tscmHighCard">
<span class="count" id="tscmHighCount">0</span>
<span class="label">High</span>
</div>
<div class="threat-card medium" id="tscmMediumCard">
<span class="count" id="tscmMediumCount">0</span>
<span class="label">Medium</span>
</div>
<div class="threat-card low" id="tscmLowCard">
<span class="count" id="tscmLowCount">0</span>
<span class="label">Low</span>
</div>
</div>
<!-- Main Content Grid -->
<div class="tscm-main-grid">
<!-- WiFi Panel -->
<div class="tscm-panel">
<div class="tscm-panel-header">
WiFi Networks
<span class="badge" id="tscmWifiCount">0</span>
</div>
<div class="tscm-panel-content" id="tscmWifiList">
<div class="tscm-empty">Start a sweep to scan for WiFi networks</div>
</div>
</div>
<!-- Bluetooth Panel -->
<div class="tscm-panel">
<div class="tscm-panel-header">
Bluetooth Devices
<span class="badge" id="tscmBtCount">0</span>
</div>
<div class="tscm-panel-content" id="tscmBtList">
<div class="tscm-empty">Start a sweep to scan for Bluetooth devices</div>
</div>
</div>
<!-- Threats Panel -->
<div class="tscm-panel" style="grid-column: span 2;">
<div class="tscm-panel-header">
Detected Threats
<span class="badge" id="tscmThreatCount">0</span>
</div>
<div class="tscm-panel-content" id="tscmThreatList">
<div class="tscm-empty">No threats detected</div>
</div>
</div>
</div>
</div>
<!-- Device Intelligence Dashboard (above waterfall for prominence) -->
<div class="recon-panel collapsed" id="reconPanel">
<div class="recon-header" onclick="toggleReconCollapse()" style="cursor: pointer;">
@@ -2347,6 +2783,7 @@
if (isBtRunning) stopBtScan();
if (isAdsbRunning) stopAdsbScan();
if (isAprsRunning) stopAprs();
if (isTscmRunning) stopTscmSweep();
currentMode = mode;
// Remove active from all nav buttons, then add to the correct one
@@ -2354,7 +2791,7 @@
const modeMap = {
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
'listening': 'listening', 'aprs': 'aprs'
'listening': 'listening', 'aprs': 'aprs', 'tscm': 'tscm'
};
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
const label = btn.querySelector('.nav-label');
@@ -2370,6 +2807,7 @@
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
document.getElementById('aprsMode').classList.toggle('active', mode === 'aprs');
document.getElementById('tscmMode').classList.toggle('active', mode === 'tscm');
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
@@ -2398,7 +2836,8 @@
'wifi': 'WIFI',
'bluetooth': 'BLUETOOTH',
'listening': 'LISTENING POST',
'aprs': 'APRS'
'aprs': 'APRS',
'tscm': 'TSCM'
};
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
@@ -2409,6 +2848,7 @@
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
document.getElementById('aprsVisuals').style.display = mode === 'aprs' ? 'flex' : 'none';
document.getElementById('tscmVisuals').style.display = mode === 'tscm' ? 'flex' : 'none';
// Update output panel title based on mode
const titles = {
@@ -2419,14 +2859,20 @@
'wifi': 'WiFi Scanner',
'bluetooth': 'Bluetooth Scanner',
'listening': 'Listening Post',
'aprs': 'APRS Tracker'
'aprs': 'APRS Tracker',
'tscm': 'TSCM Counter-Surveillance'
};
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft)
// Initialize TSCM mode when selected
if (mode === 'tscm') {
loadTscmBaselines();
}
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening' || mode === 'aprs') {
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening' || mode === 'aprs' || mode === 'tscm') {
document.getElementById('reconPanel').style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none';
@@ -8971,6 +9417,365 @@
});
// NOTE: Scanner and Audio Receiver code moved to static/js/modes/listening-post.js
// ============================================
// TSCM (Counter-Surveillance) Functions
// ============================================
let isTscmRunning = false;
let tscmEventSource = null;
let tscmThreats = [];
let tscmWifiDevices = [];
let tscmBtDevices = [];
let isRecordingBaseline = false;
async function loadTscmBaselines() {
try {
const response = await fetch('/tscm/baselines');
const data = await response.json();
const select = document.getElementById('tscmBaselineSelect');
select.innerHTML = '<option value="">No Baseline</option>';
if (data.baselines) {
data.baselines.forEach(b => {
const opt = document.createElement('option');
opt.value = b.id;
opt.textContent = b.name + (b.is_active ? ' (Active)' : '');
select.appendChild(opt);
});
}
} catch (e) {
console.error('Failed to load baselines:', e);
}
}
async function startTscmSweep() {
const sweepType = document.getElementById('tscmSweepType').value;
const baselineId = document.getElementById('tscmBaselineSelect').value || null;
const wifiEnabled = document.getElementById('tscmWifiEnabled').checked;
const btEnabled = document.getElementById('tscmBtEnabled').checked;
const rfEnabled = document.getElementById('tscmRfEnabled').checked;
// Clear any previous warnings
document.getElementById('tscmDeviceWarnings').style.display = 'none';
document.getElementById('tscmDeviceWarnings').innerHTML = '';
try {
const response = await fetch('/tscm/sweep/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sweep_type: sweepType,
baseline_id: baselineId ? parseInt(baselineId) : null,
wifi: wifiEnabled,
bluetooth: btEnabled,
rf: rfEnabled
})
});
const data = await response.json();
if (data.status === 'success') {
isTscmRunning = true;
document.getElementById('startTscmBtn').style.display = 'none';
document.getElementById('stopTscmBtn').style.display = 'block';
document.getElementById('tscmProgress').style.display = 'flex';
// Show warnings if any devices unavailable
if (data.warnings && data.warnings.length > 0) {
const warningsDiv = document.getElementById('tscmDeviceWarnings');
warningsDiv.innerHTML = data.warnings.map(w =>
`<div style="color: #ff9933; font-size: 10px; margin-bottom: 2px;">⚠ ${w}</div>`
).join('');
warningsDiv.style.display = 'block';
}
// Update device indicators
updateTscmDeviceIndicators(data.devices);
// Reset displays
tscmThreats = [];
tscmWifiDevices = [];
tscmBtDevices = [];
updateTscmDisplays();
// Start SSE stream
startTscmStream();
} else {
// Show error with details
let errorMsg = data.message || 'Failed to start sweep';
if (data.details && data.details.length > 0) {
errorMsg += '\n\n' + data.details.join('\n');
}
alert(errorMsg);
}
} catch (e) {
console.error('Failed to start TSCM sweep:', e);
alert('Failed to start sweep: Network error');
}
}
function updateTscmDeviceIndicators(devices) {
const wifiIndicator = document.getElementById('tscmWifiIndicator');
const btIndicator = document.getElementById('tscmBtIndicator');
const rfIndicator = document.getElementById('tscmRfIndicator');
if (wifiIndicator) {
wifiIndicator.classList.toggle('active', devices.wifi);
wifiIndicator.classList.toggle('inactive', !devices.wifi);
}
if (btIndicator) {
btIndicator.classList.toggle('active', devices.bluetooth);
btIndicator.classList.toggle('inactive', !devices.bluetooth);
}
if (rfIndicator) {
rfIndicator.classList.toggle('active', devices.rf);
rfIndicator.classList.toggle('inactive', !devices.rf);
}
}
async function stopTscmSweep() {
try {
await fetch('/tscm/sweep/stop', { method: 'POST' });
} catch (e) {
console.error('Error stopping sweep:', e);
}
isTscmRunning = false;
if (tscmEventSource) {
tscmEventSource.close();
tscmEventSource = null;
}
document.getElementById('startTscmBtn').style.display = 'block';
document.getElementById('stopTscmBtn').style.display = 'none';
document.getElementById('tscmProgress').style.display = 'none';
}
function startTscmStream() {
if (tscmEventSource) {
tscmEventSource.close();
}
tscmEventSource = new EventSource('/tscm/sweep/stream');
tscmEventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
handleTscmEvent(data);
} catch (e) {
console.error('TSCM SSE parse error:', e);
}
};
tscmEventSource.onerror = function() {
console.warn('TSCM SSE connection error');
};
}
function handleTscmEvent(data) {
switch (data.type) {
case 'sweep_progress':
updateTscmProgress(data);
break;
case 'threat_detected':
addTscmThreat(data);
break;
case 'sweep_completed':
completeTscmSweep(data);
break;
case 'sweep_stopped':
case 'sweep_error':
stopTscmSweep();
break;
}
}
function updateTscmProgress(data) {
// Update percentage text
document.getElementById('tscmProgressPercent').textContent = data.progress + '%';
// Update SVG circle progress (circumference = 2 * PI * 45 = ~283)
const circumference = 283;
const offset = circumference - (data.progress / 100) * circumference;
const circle = document.getElementById('tscmScannerCircle');
if (circle) {
circle.style.strokeDashoffset = offset;
}
// Update status text
const statusText = data.threats_found > 0
? `THREATS: ${data.threats_found}`
: `SCANNING ${data.wifi_count}W ${data.bt_count}B`;
document.getElementById('tscmProgressLabel').textContent = statusText;
// Update counts in sidebar
const criticalEl = document.querySelector('#tscmThreatSummary .threat-card.critical .count');
const highEl = document.querySelector('#tscmThreatSummary .threat-card.high .count');
const mediumEl = document.querySelector('#tscmThreatSummary .threat-card.medium .count');
const lowEl = document.querySelector('#tscmThreatSummary .threat-card.low .count');
if (criticalEl) criticalEl.textContent = data.critical || 0;
if (highEl) highEl.textContent = data.high || 0;
if (mediumEl) mediumEl.textContent = data.medium || 0;
if (lowEl) lowEl.textContent = data.low || 0;
}
function addTscmThreat(threat) {
tscmThreats.unshift(threat);
// Update dashboard counts
updateTscmThreatCounts();
updateTscmDisplays();
}
function updateTscmThreatCounts() {
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
tscmThreats.forEach(t => {
if (counts[t.severity] !== undefined) {
counts[t.severity]++;
}
});
document.getElementById('tscmCriticalCount').textContent = counts.critical;
document.getElementById('tscmHighCount').textContent = counts.high;
document.getElementById('tscmMediumCount').textContent = counts.medium;
document.getElementById('tscmLowCount').textContent = counts.low;
document.getElementById('tscmCriticalCard').classList.toggle('active', counts.critical > 0);
document.getElementById('tscmHighCard').classList.toggle('active', counts.high > 0);
document.getElementById('tscmMediumCard').classList.toggle('active', counts.medium > 0);
document.getElementById('tscmLowCard').classList.toggle('active', counts.low > 0);
document.getElementById('tscmThreatCount').textContent = tscmThreats.length;
}
function updateTscmDisplays() {
// Update WiFi list
const wifiList = document.getElementById('tscmWifiList');
if (tscmWifiDevices.length === 0) {
wifiList.innerHTML = '<div class="tscm-empty">No WiFi networks detected</div>';
} else {
wifiList.innerHTML = tscmWifiDevices.map(d => `
<div class="tscm-device-item ${d.is_threat ? 'threat' : (d.is_new ? 'new' : 'baseline')}">
<div class="tscm-device-name">${escapeHtml(d.ssid || d.bssid || 'Hidden')}</div>
<div class="tscm-device-meta">
<span>${d.bssid}</span>
<span>${d.signal || '--'} dBm</span>
</div>
</div>
`).join('');
}
document.getElementById('tscmWifiCount').textContent = tscmWifiDevices.length;
// Update BT list
const btList = document.getElementById('tscmBtList');
if (tscmBtDevices.length === 0) {
btList.innerHTML = '<div class="tscm-empty">No Bluetooth devices detected</div>';
} else {
btList.innerHTML = tscmBtDevices.map(d => `
<div class="tscm-device-item ${d.is_threat ? 'threat' : (d.is_new ? 'new' : 'baseline')}">
<div class="tscm-device-name">${escapeHtml(d.name || 'Unknown')}</div>
<div class="tscm-device-meta">
<span>${d.mac}</span>
<span>${d.rssi || '--'} dBm</span>
</div>
</div>
`).join('');
}
document.getElementById('tscmBtCount').textContent = tscmBtDevices.length;
// Update threats list
const threatList = document.getElementById('tscmThreatList');
if (tscmThreats.length === 0) {
threatList.innerHTML = '<div class="tscm-empty">No threats detected</div>';
} else {
threatList.innerHTML = '<div class="tscm-threat-list">' + tscmThreats.map(t => `
<div class="tscm-threat-item ${t.severity}">
<div class="tscm-threat-header">
<span class="tscm-threat-type">${escapeHtml(t.threat_type || 'Unknown')}</span>
<span class="tscm-threat-severity">${t.severity}</span>
</div>
<div class="tscm-threat-details">
<strong>${escapeHtml(t.name || t.identifier)}</strong><br>
Source: ${t.source} | Signal: ${t.signal_strength || '--'} dBm
</div>
</div>
`).join('') + '</div>';
}
}
function completeTscmSweep(data) {
isTscmRunning = false;
if (tscmEventSource) {
tscmEventSource.close();
tscmEventSource = null;
}
document.getElementById('startTscmBtn').style.display = 'block';
document.getElementById('stopTscmBtn').style.display = 'none';
document.getElementById('tscmProgress').style.display = 'none';
document.getElementById('tscmProgressLabel').textContent = 'Sweep Complete';
document.getElementById('tscmProgressPercent').textContent = '100%';
document.getElementById('tscmProgressBar').style.width = '100%';
// Final update of counts
updateTscmThreatCounts();
}
async function tscmRecordBaseline() {
const name = document.getElementById('tscmBaselineName').value ||
`Baseline ${new Date().toLocaleString()}`;
try {
const response = await fetch('/tscm/baseline/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name })
});
const data = await response.json();
if (data.status === 'success') {
isRecordingBaseline = true;
document.getElementById('tscmRecordBaselineBtn').style.display = 'none';
document.getElementById('tscmStopBaselineBtn').style.display = 'block';
document.getElementById('tscmBaselineStatus').textContent = 'Recording baseline...';
document.getElementById('tscmBaselineStatus').style.color = '#ff9933';
} else {
alert(data.message || 'Failed to start baseline recording');
}
} catch (e) {
console.error('Failed to start baseline:', e);
alert('Failed to start baseline recording');
}
}
async function tscmStopBaseline() {
try {
const response = await fetch('/tscm/baseline/stop', { method: 'POST' });
const data = await response.json();
isRecordingBaseline = false;
document.getElementById('tscmRecordBaselineBtn').style.display = 'block';
document.getElementById('tscmStopBaselineBtn').style.display = 'none';
if (data.status === 'success') {
document.getElementById('tscmBaselineStatus').textContent =
`Baseline saved: ${data.wifi_count} WiFi, ${data.bt_count} BT, ${data.rf_count} RF`;
document.getElementById('tscmBaselineStatus').style.color = '#00ff88';
loadTscmBaselines();
} else {
document.getElementById('tscmBaselineStatus').textContent = data.message || 'Recording stopped';
document.getElementById('tscmBaselineStatus').style.color = 'var(--text-muted)';
}
} catch (e) {
console.error('Failed to stop baseline:', e);
document.getElementById('tscmBaselineStatus').textContent = 'Error stopping baseline';
}
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
</script>
<!-- Scanner/Audio code moved to static/js/modes/listening-post.js -->

View File

@@ -100,6 +100,100 @@ def init_db() -> None:
)
''')
# =====================================================================
# TSCM (Technical Surveillance Countermeasures) Tables
# =====================================================================
# TSCM Baselines - Environment snapshots for comparison
conn.execute('''
CREATE TABLE IF NOT EXISTS tscm_baselines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
location TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
wifi_networks TEXT,
bt_devices TEXT,
rf_frequencies TEXT,
gps_coords TEXT,
is_active BOOLEAN DEFAULT 0
)
''')
# TSCM Sweeps - Individual sweep sessions
conn.execute('''
CREATE TABLE IF NOT EXISTS tscm_sweeps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
baseline_id INTEGER,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
status TEXT DEFAULT 'running',
sweep_type TEXT,
wifi_enabled BOOLEAN DEFAULT 1,
bt_enabled BOOLEAN DEFAULT 1,
rf_enabled BOOLEAN DEFAULT 1,
results TEXT,
anomalies TEXT,
threats_found INTEGER DEFAULT 0,
FOREIGN KEY (baseline_id) REFERENCES tscm_baselines(id)
)
''')
# TSCM Threats - Detected threats/anomalies
conn.execute('''
CREATE TABLE IF NOT EXISTS tscm_threats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sweep_id INTEGER,
detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
threat_type TEXT NOT NULL,
severity TEXT DEFAULT 'medium',
source TEXT,
identifier TEXT,
name TEXT,
signal_strength INTEGER,
frequency REAL,
details TEXT,
acknowledged BOOLEAN DEFAULT 0,
notes TEXT,
gps_coords TEXT,
FOREIGN KEY (sweep_id) REFERENCES tscm_sweeps(id)
)
''')
# TSCM Scheduled Sweeps
conn.execute('''
CREATE TABLE IF NOT EXISTS tscm_schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
baseline_id INTEGER,
zone_name TEXT,
cron_expression TEXT,
sweep_type TEXT DEFAULT 'standard',
enabled BOOLEAN DEFAULT 1,
last_run TIMESTAMP,
next_run TIMESTAMP,
notify_on_threat BOOLEAN DEFAULT 1,
notify_email TEXT,
FOREIGN KEY (baseline_id) REFERENCES tscm_baselines(id)
)
''')
# TSCM indexes for performance
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_tscm_threats_sweep
ON tscm_threats(sweep_id)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_tscm_threats_severity
ON tscm_threats(severity, detected_at)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_tscm_sweeps_baseline
ON tscm_sweeps(baseline_id)
''')
logger.info("Database initialized successfully")
@@ -349,3 +443,353 @@ def get_correlations(min_confidence: float = 0.5) -> list[dict]:
})
return results
# =============================================================================
# TSCM Functions
# =============================================================================
def create_tscm_baseline(
name: str,
location: str | None = None,
description: str | None = None,
wifi_networks: list | None = None,
bt_devices: list | None = None,
rf_frequencies: list | None = None,
gps_coords: dict | None = None
) -> int:
"""
Create a new TSCM baseline.
Returns:
The ID of the created 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,
json.dumps(wifi_networks) if wifi_networks 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
))
return cursor.lastrowid
def get_tscm_baseline(baseline_id: int) -> dict | None:
"""Get a specific TSCM baseline by ID."""
with get_db() as conn:
cursor = conn.execute('''
SELECT * FROM tscm_baselines WHERE id = ?
''', (baseline_id,))
row = cursor.fetchone()
if row is None:
return None
return {
'id': row['id'],
'name': row['name'],
'location': row['location'],
'description': row['description'],
'created_at': row['created_at'],
'wifi_networks': json.loads(row['wifi_networks']) if row['wifi_networks'] else [],
'bt_devices': json.loads(row['bt_devices']) if row['bt_devices'] else [],
'rf_frequencies': json.loads(row['rf_frequencies']) if row['rf_frequencies'] else [],
'gps_coords': json.loads(row['gps_coords']) if row['gps_coords'] else None,
'is_active': bool(row['is_active'])
}
def get_all_tscm_baselines() -> list[dict]:
"""Get all TSCM baselines."""
with get_db() as conn:
cursor = conn.execute('''
SELECT id, name, location, description, created_at, is_active
FROM tscm_baselines
ORDER BY created_at DESC
''')
return [dict(row) for row in cursor]
def get_active_tscm_baseline() -> dict | None:
"""Get the currently active TSCM baseline."""
with get_db() as conn:
cursor = conn.execute('''
SELECT * FROM tscm_baselines WHERE is_active = 1 LIMIT 1
''')
row = cursor.fetchone()
if row is None:
return None
return get_tscm_baseline(row['id'])
def set_active_tscm_baseline(baseline_id: int) -> bool:
"""Set a baseline as active (deactivates others)."""
with get_db() as conn:
# Deactivate all
conn.execute('UPDATE tscm_baselines SET is_active = 0')
# Activate selected
cursor = conn.execute(
'UPDATE tscm_baselines SET is_active = 1 WHERE id = ?',
(baseline_id,)
)
return cursor.rowcount > 0
def update_tscm_baseline(
baseline_id: int,
wifi_networks: list | None = None,
bt_devices: list | None = None,
rf_frequencies: list | None = None
) -> bool:
"""Update baseline device lists."""
updates = []
params = []
if wifi_networks is not None:
updates.append('wifi_networks = ?')
params.append(json.dumps(wifi_networks))
if bt_devices is not None:
updates.append('bt_devices = ?')
params.append(json.dumps(bt_devices))
if rf_frequencies is not None:
updates.append('rf_frequencies = ?')
params.append(json.dumps(rf_frequencies))
if not updates:
return False
params.append(baseline_id)
with get_db() as conn:
cursor = conn.execute(
f'UPDATE tscm_baselines SET {", ".join(updates)} WHERE id = ?',
params
)
return cursor.rowcount > 0
def delete_tscm_baseline(baseline_id: int) -> bool:
"""Delete a TSCM baseline."""
with get_db() as conn:
cursor = conn.execute(
'DELETE FROM tscm_baselines WHERE id = ?',
(baseline_id,)
)
return cursor.rowcount > 0
def create_tscm_sweep(
sweep_type: str,
baseline_id: int | None = None,
wifi_enabled: bool = True,
bt_enabled: bool = True,
rf_enabled: bool = True
) -> int:
"""
Create a new TSCM sweep session.
Returns:
The ID of the created sweep
"""
with get_db() as conn:
cursor = conn.execute('''
INSERT INTO tscm_sweeps
(baseline_id, sweep_type, wifi_enabled, bt_enabled, rf_enabled)
VALUES (?, ?, ?, ?, ?)
''', (baseline_id, sweep_type, wifi_enabled, bt_enabled, rf_enabled))
return cursor.lastrowid
def update_tscm_sweep(
sweep_id: int,
status: str | None = None,
results: dict | None = None,
anomalies: list | None = None,
threats_found: int | None = None,
completed: bool = False
) -> bool:
"""Update a TSCM sweep."""
updates = []
params = []
if status is not None:
updates.append('status = ?')
params.append(status)
if results is not None:
updates.append('results = ?')
params.append(json.dumps(results))
if anomalies is not None:
updates.append('anomalies = ?')
params.append(json.dumps(anomalies))
if threats_found is not None:
updates.append('threats_found = ?')
params.append(threats_found)
if completed:
updates.append('completed_at = CURRENT_TIMESTAMP')
if not updates:
return False
params.append(sweep_id)
with get_db() as conn:
cursor = conn.execute(
f'UPDATE tscm_sweeps SET {", ".join(updates)} WHERE id = ?',
params
)
return cursor.rowcount > 0
def get_tscm_sweep(sweep_id: int) -> dict | None:
"""Get a specific TSCM sweep by ID."""
with get_db() as conn:
cursor = conn.execute('SELECT * FROM tscm_sweeps WHERE id = ?', (sweep_id,))
row = cursor.fetchone()
if row is None:
return None
return {
'id': row['id'],
'baseline_id': row['baseline_id'],
'started_at': row['started_at'],
'completed_at': row['completed_at'],
'status': row['status'],
'sweep_type': row['sweep_type'],
'wifi_enabled': bool(row['wifi_enabled']),
'bt_enabled': bool(row['bt_enabled']),
'rf_enabled': bool(row['rf_enabled']),
'results': json.loads(row['results']) if row['results'] else None,
'anomalies': json.loads(row['anomalies']) if row['anomalies'] else [],
'threats_found': row['threats_found']
}
def add_tscm_threat(
sweep_id: int,
threat_type: str,
severity: str,
source: str,
identifier: str,
name: str | None = None,
signal_strength: int | None = None,
frequency: float | None = None,
details: dict | None = None,
gps_coords: dict | None = None
) -> int:
"""
Add a detected threat to a TSCM sweep.
Returns:
The ID of the created threat
"""
with get_db() as conn:
cursor = conn.execute('''
INSERT INTO tscm_threats
(sweep_id, threat_type, severity, source, identifier, name,
signal_strength, frequency, details, gps_coords)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
sweep_id, threat_type, severity, source, identifier, name,
signal_strength, frequency,
json.dumps(details) if details else None,
json.dumps(gps_coords) if gps_coords else None
))
return cursor.lastrowid
def get_tscm_threats(
sweep_id: int | None = None,
severity: str | None = None,
acknowledged: bool | None = None,
limit: int = 100
) -> list[dict]:
"""Get TSCM threats with optional filters."""
conditions = []
params = []
if sweep_id is not None:
conditions.append('sweep_id = ?')
params.append(sweep_id)
if severity is not None:
conditions.append('severity = ?')
params.append(severity)
if acknowledged is not None:
conditions.append('acknowledged = ?')
params.append(1 if acknowledged else 0)
where_clause = f'WHERE {" AND ".join(conditions)}' if conditions else ''
params.append(limit)
with get_db() as conn:
cursor = conn.execute(f'''
SELECT * FROM tscm_threats
{where_clause}
ORDER BY detected_at DESC
LIMIT ?
''', params)
results = []
for row in cursor:
results.append({
'id': row['id'],
'sweep_id': row['sweep_id'],
'detected_at': row['detected_at'],
'threat_type': row['threat_type'],
'severity': row['severity'],
'source': row['source'],
'identifier': row['identifier'],
'name': row['name'],
'signal_strength': row['signal_strength'],
'frequency': row['frequency'],
'details': json.loads(row['details']) if row['details'] else None,
'acknowledged': bool(row['acknowledged']),
'notes': row['notes'],
'gps_coords': json.loads(row['gps_coords']) if row['gps_coords'] else None
})
return results
def acknowledge_tscm_threat(threat_id: int, notes: str | None = None) -> bool:
"""Acknowledge a TSCM threat."""
with get_db() as conn:
if notes:
cursor = conn.execute(
'UPDATE tscm_threats SET acknowledged = 1, notes = ? WHERE id = ?',
(notes, threat_id)
)
else:
cursor = conn.execute(
'UPDATE tscm_threats SET acknowledged = 1 WHERE id = ?',
(threat_id,)
)
return cursor.rowcount > 0
def get_tscm_threat_summary() -> dict:
"""Get summary counts of threats by severity."""
with get_db() as conn:
cursor = conn.execute('''
SELECT severity, COUNT(*) as count
FROM tscm_threats
WHERE acknowledged = 0
GROUP BY severity
''')
summary = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0, 'total': 0}
for row in cursor:
summary[row['severity']] = row['count']
summary['total'] += row['count']
return summary

View File

@@ -311,6 +311,56 @@ TOOL_DEPENDENCIES = {
}
}
}
},
'tscm': {
'name': 'TSCM Counter-Surveillance',
'tools': {
'rtl_power': {
'required': False,
'description': 'Wideband spectrum sweep for RF analysis',
'install': {
'apt': 'sudo apt install rtl-sdr',
'brew': 'brew install librtlsdr',
'manual': 'https://osmocom.org/projects/rtl-sdr/wiki'
}
},
'rtl_fm': {
'required': True,
'description': 'RF signal demodulation',
'install': {
'apt': 'sudo apt install rtl-sdr',
'brew': 'brew install librtlsdr',
'manual': 'https://osmocom.org/projects/rtl-sdr/wiki'
}
},
'rtl_433': {
'required': False,
'description': 'ISM band device decoding',
'install': {
'apt': 'sudo apt install rtl-433',
'brew': 'brew install rtl_433',
'manual': 'https://github.com/merbanan/rtl_433'
}
},
'airmon-ng': {
'required': False,
'description': 'WiFi monitor mode for network scanning',
'install': {
'apt': 'sudo apt install aircrack-ng',
'brew': 'Not available on macOS',
'manual': 'https://aircrack-ng.org'
}
},
'bluetoothctl': {
'required': False,
'description': 'Bluetooth device scanning',
'install': {
'apt': 'sudo apt install bluez',
'brew': 'Not available on macOS (use native)',
'manual': 'http://www.bluez.org'
}
}
}
}
}

10
utils/tscm/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
"""
TSCM (Technical Surveillance Countermeasures) Utilities Package
Provides baseline recording, threat detection, and analysis tools
for counter-surveillance operations.
"""
from __future__ import annotations
__all__ = ['detector', 'baseline']

388
utils/tscm/baseline.py Normal file
View File

@@ -0,0 +1,388 @@
"""
TSCM Baseline Recording and Comparison
Records environment "fingerprints" and compares current scans
against baselines to detect new or anomalous devices.
"""
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any
from utils.database import (
create_tscm_baseline,
get_active_tscm_baseline,
get_tscm_baseline,
update_tscm_baseline,
)
logger = logging.getLogger('intercept.tscm.baseline')
class BaselineRecorder:
"""
Records and manages TSCM environment baselines.
"""
def __init__(self):
self.recording = False
self.current_baseline_id: int | None = None
self.wifi_networks: dict[str, dict] = {} # BSSID -> network info
self.bt_devices: dict[str, dict] = {} # MAC -> device info
self.rf_frequencies: dict[float, dict] = {} # Frequency -> signal info
def start_recording(
self,
name: str,
location: str | None = None,
description: str | None = None
) -> int:
"""
Start recording a new baseline.
Args:
name: Baseline name
location: Optional location description
description: Optional description
Returns:
Baseline ID
"""
self.recording = True
self.wifi_networks = {}
self.bt_devices = {}
self.rf_frequencies = {}
# Create baseline in database
self.current_baseline_id = create_tscm_baseline(
name=name,
location=location,
description=description
)
logger.info(f"Started baseline recording: {name} (ID: {self.current_baseline_id})")
return self.current_baseline_id
def stop_recording(self) -> dict:
"""
Stop recording and finalize baseline.
Returns:
Final baseline summary
"""
if not self.recording or not self.current_baseline_id:
return {'error': 'Not recording'}
self.recording = False
# Convert to lists for storage
wifi_list = list(self.wifi_networks.values())
bt_list = list(self.bt_devices.values())
rf_list = list(self.rf_frequencies.values())
# Update database
update_tscm_baseline(
self.current_baseline_id,
wifi_networks=wifi_list,
bt_devices=bt_list,
rf_frequencies=rf_list
)
summary = {
'baseline_id': self.current_baseline_id,
'wifi_count': len(wifi_list),
'bt_count': len(bt_list),
'rf_count': len(rf_list),
}
logger.info(
f"Baseline recording complete: {summary['wifi_count']} WiFi, "
f"{summary['bt_count']} BT, {summary['rf_count']} RF"
)
baseline_id = self.current_baseline_id
self.current_baseline_id = None
return summary
def add_wifi_device(self, device: dict) -> None:
"""Add a WiFi device to the current baseline."""
if not self.recording:
return
mac = device.get('bssid', device.get('mac', '')).upper()
if not mac:
return
# Update or add device
if mac in self.wifi_networks:
# Update with latest info
self.wifi_networks[mac].update({
'last_seen': datetime.now().isoformat(),
'power': device.get('power', self.wifi_networks[mac].get('power')),
})
else:
self.wifi_networks[mac] = {
'bssid': mac,
'essid': device.get('essid', device.get('ssid', '')),
'channel': device.get('channel'),
'power': device.get('power', device.get('signal')),
'vendor': device.get('vendor', ''),
'encryption': device.get('privacy', device.get('encryption', '')),
'first_seen': datetime.now().isoformat(),
'last_seen': datetime.now().isoformat(),
}
def add_bt_device(self, device: dict) -> None:
"""Add a Bluetooth device to the current baseline."""
if not self.recording:
return
mac = device.get('mac', device.get('address', '')).upper()
if not mac:
return
if mac in self.bt_devices:
self.bt_devices[mac].update({
'last_seen': datetime.now().isoformat(),
'rssi': device.get('rssi', self.bt_devices[mac].get('rssi')),
})
else:
self.bt_devices[mac] = {
'mac': mac,
'name': device.get('name', ''),
'rssi': device.get('rssi', device.get('signal')),
'manufacturer': device.get('manufacturer', ''),
'type': device.get('type', ''),
'first_seen': datetime.now().isoformat(),
'last_seen': datetime.now().isoformat(),
}
def add_rf_signal(self, signal: dict) -> None:
"""Add an RF signal to the current baseline."""
if not self.recording:
return
frequency = signal.get('frequency')
if not frequency:
return
# Round to 0.1 MHz for grouping
freq_key = round(frequency, 1)
if freq_key in self.rf_frequencies:
existing = self.rf_frequencies[freq_key]
existing['last_seen'] = datetime.now().isoformat()
existing['hit_count'] = existing.get('hit_count', 1) + 1
# Update max signal level
new_level = signal.get('level', signal.get('power', -100))
if new_level > existing.get('max_level', -100):
existing['max_level'] = new_level
else:
self.rf_frequencies[freq_key] = {
'frequency': freq_key,
'level': signal.get('level', signal.get('power')),
'max_level': signal.get('level', signal.get('power', -100)),
'modulation': signal.get('modulation', ''),
'first_seen': datetime.now().isoformat(),
'last_seen': datetime.now().isoformat(),
'hit_count': 1,
}
def get_recording_status(self) -> dict:
"""Get current recording status and counts."""
return {
'recording': self.recording,
'baseline_id': self.current_baseline_id,
'wifi_count': len(self.wifi_networks),
'bt_count': len(self.bt_devices),
'rf_count': len(self.rf_frequencies),
}
class BaselineComparator:
"""
Compares current scan results against a baseline.
"""
def __init__(self, baseline: dict):
"""
Initialize comparator with a baseline.
Args:
baseline: Baseline dict from database
"""
self.baseline = baseline
self.baseline_wifi = {
d.get('bssid', d.get('mac', '')).upper(): d
for d in baseline.get('wifi_networks', [])
if d.get('bssid') or d.get('mac')
}
self.baseline_bt = {
d.get('mac', d.get('address', '')).upper(): d
for d in baseline.get('bt_devices', [])
if d.get('mac') or d.get('address')
}
self.baseline_rf = {
round(d.get('frequency', 0), 1): d
for d in baseline.get('rf_frequencies', [])
if d.get('frequency')
}
def compare_wifi(self, current_devices: list[dict]) -> dict:
"""
Compare current WiFi devices against baseline.
Returns:
Dict with new, missing, and matching devices
"""
current_macs = {
d.get('bssid', d.get('mac', '')).upper(): d
for d in current_devices
if d.get('bssid') or d.get('mac')
}
new_devices = []
missing_devices = []
matching_devices = []
# Find new devices
for mac, device in current_macs.items():
if mac not in self.baseline_wifi:
new_devices.append(device)
else:
matching_devices.append(device)
# Find missing devices
for mac, device in self.baseline_wifi.items():
if mac not in current_macs:
missing_devices.append(device)
return {
'new': new_devices,
'missing': missing_devices,
'matching': matching_devices,
'new_count': len(new_devices),
'missing_count': len(missing_devices),
'matching_count': len(matching_devices),
}
def compare_bluetooth(self, current_devices: list[dict]) -> dict:
"""Compare current Bluetooth devices against baseline."""
current_macs = {
d.get('mac', d.get('address', '')).upper(): d
for d in current_devices
if d.get('mac') or d.get('address')
}
new_devices = []
missing_devices = []
matching_devices = []
for mac, device in current_macs.items():
if mac not in self.baseline_bt:
new_devices.append(device)
else:
matching_devices.append(device)
for mac, device in self.baseline_bt.items():
if mac not in current_macs:
missing_devices.append(device)
return {
'new': new_devices,
'missing': missing_devices,
'matching': matching_devices,
'new_count': len(new_devices),
'missing_count': len(missing_devices),
'matching_count': len(matching_devices),
}
def compare_rf(self, current_signals: list[dict]) -> dict:
"""Compare current RF signals against baseline."""
current_freqs = {
round(s.get('frequency', 0), 1): s
for s in current_signals
if s.get('frequency')
}
new_signals = []
missing_signals = []
matching_signals = []
for freq, signal in current_freqs.items():
if freq not in self.baseline_rf:
new_signals.append(signal)
else:
matching_signals.append(signal)
for freq, signal in self.baseline_rf.items():
if freq not in current_freqs:
missing_signals.append(signal)
return {
'new': new_signals,
'missing': missing_signals,
'matching': matching_signals,
'new_count': len(new_signals),
'missing_count': len(missing_signals),
'matching_count': len(matching_signals),
}
def compare_all(
self,
wifi_devices: list[dict] | None = None,
bt_devices: list[dict] | None = None,
rf_signals: list[dict] | None = None
) -> dict:
"""
Compare all current data against baseline.
Returns:
Dict with comparison results for each category
"""
results = {
'wifi': None,
'bluetooth': None,
'rf': None,
'total_new': 0,
'total_missing': 0,
}
if wifi_devices is not None:
results['wifi'] = self.compare_wifi(wifi_devices)
results['total_new'] += results['wifi']['new_count']
results['total_missing'] += results['wifi']['missing_count']
if bt_devices is not None:
results['bluetooth'] = self.compare_bluetooth(bt_devices)
results['total_new'] += results['bluetooth']['new_count']
results['total_missing'] += results['bluetooth']['missing_count']
if rf_signals is not None:
results['rf'] = self.compare_rf(rf_signals)
results['total_new'] += results['rf']['new_count']
results['total_missing'] += results['rf']['missing_count']
return results
def get_comparison_for_active_baseline(
wifi_devices: list[dict] | None = None,
bt_devices: list[dict] | None = None,
rf_signals: list[dict] | None = None
) -> dict | None:
"""
Convenience function to compare against the active baseline.
Returns:
Comparison results or None if no active baseline
"""
baseline = get_active_tscm_baseline()
if not baseline:
return None
comparator = BaselineComparator(baseline)
return comparator.compare_all(wifi_devices, bt_devices, rf_signals)

324
utils/tscm/detector.py Normal file
View File

@@ -0,0 +1,324 @@
"""
TSCM Threat Detection Engine
Analyzes WiFi, Bluetooth, and RF data to identify potential surveillance devices
and classify threats based on known patterns and baseline comparison.
"""
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any
from data.tscm_frequencies import (
BLE_TRACKER_SIGNATURES,
THREAT_TYPES,
WIFI_CAMERA_PATTERNS,
get_frequency_risk,
get_threat_severity,
is_known_tracker,
is_potential_camera,
)
logger = logging.getLogger('intercept.tscm.detector')
class ThreatDetector:
"""
Analyzes scan results to detect potential surveillance threats.
"""
def __init__(self, baseline: dict | None = None):
"""
Initialize the threat detector.
Args:
baseline: Optional baseline dict containing expected devices
"""
self.baseline = baseline
self.baseline_wifi_macs = set()
self.baseline_bt_macs = set()
self.baseline_rf_freqs = set()
if baseline:
self._load_baseline(baseline)
def _load_baseline(self, baseline: dict) -> None:
"""Load baseline device identifiers for comparison."""
# WiFi networks and clients
for network in baseline.get('wifi_networks', []):
if 'bssid' in network:
self.baseline_wifi_macs.add(network['bssid'].upper())
if 'clients' in network:
for client in network['clients']:
if 'mac' in client:
self.baseline_wifi_macs.add(client['mac'].upper())
# Bluetooth devices
for device in baseline.get('bt_devices', []):
if 'mac' in device:
self.baseline_bt_macs.add(device['mac'].upper())
# RF frequencies (rounded to nearest 0.1 MHz)
for freq in baseline.get('rf_frequencies', []):
if isinstance(freq, dict):
self.baseline_rf_freqs.add(round(freq.get('frequency', 0), 1))
else:
self.baseline_rf_freqs.add(round(freq, 1))
logger.info(
f"Loaded baseline: {len(self.baseline_wifi_macs)} WiFi, "
f"{len(self.baseline_bt_macs)} BT, {len(self.baseline_rf_freqs)} RF"
)
def analyze_wifi_device(self, device: dict) -> dict | None:
"""
Analyze a WiFi device for threats.
Args:
device: WiFi device dict with bssid, essid, etc.
Returns:
Threat dict if threat detected, None otherwise
"""
mac = device.get('bssid', device.get('mac', '')).upper()
ssid = device.get('essid', device.get('ssid', ''))
vendor = device.get('vendor', '')
signal = device.get('power', device.get('signal', -100))
threats = []
# Check if new device (not in baseline)
if self.baseline and mac and mac not in self.baseline_wifi_macs:
threats.append({
'type': 'new_device',
'severity': get_threat_severity('new_device', {'signal_strength': signal}),
'reason': 'Device not present in baseline',
})
# Check for hidden camera patterns
if is_potential_camera(ssid=ssid, mac=mac, vendor=vendor):
threats.append({
'type': 'hidden_camera',
'severity': get_threat_severity('hidden_camera', {'signal_strength': signal}),
'reason': 'Device matches WiFi camera patterns',
})
# Check for hidden SSID with strong signal
if not ssid and signal and signal > -60:
threats.append({
'type': 'anomaly',
'severity': 'medium',
'reason': 'Hidden SSID with strong signal',
})
if not threats:
return None
# Return highest severity threat
threats.sort(key=lambda t: ['low', 'medium', 'high', 'critical'].index(t['severity']), reverse=True)
return {
'threat_type': threats[0]['type'],
'severity': threats[0]['severity'],
'source': 'wifi',
'identifier': mac,
'name': ssid or 'Hidden Network',
'signal_strength': signal,
'details': {
'all_threats': threats,
'vendor': vendor,
'ssid': ssid,
}
}
def analyze_bt_device(self, device: dict) -> dict | None:
"""
Analyze a Bluetooth device for threats.
Args:
device: BT device dict with mac, name, rssi, etc.
Returns:
Threat dict if threat detected, None otherwise
"""
mac = device.get('mac', device.get('address', '')).upper()
name = device.get('name', '')
rssi = device.get('rssi', device.get('signal', -100))
manufacturer = device.get('manufacturer', '')
device_type = device.get('type', '')
manufacturer_data = device.get('manufacturer_data')
threats = []
# Check if new device (not in baseline)
if self.baseline and mac and mac not in self.baseline_bt_macs:
threats.append({
'type': 'new_device',
'severity': get_threat_severity('new_device', {'signal_strength': rssi}),
'reason': 'Device not present in baseline',
})
# Check for known trackers
tracker_info = is_known_tracker(name, manufacturer_data)
if tracker_info:
threats.append({
'type': 'tracker',
'severity': tracker_info.get('risk', 'high'),
'reason': f"Known tracker detected: {tracker_info.get('name', 'Unknown')}",
'tracker_type': tracker_info.get('name'),
})
# Check for suspicious BLE beacons (unnamed, persistent)
if not name and rssi and rssi > -70:
threats.append({
'type': 'anomaly',
'severity': 'medium',
'reason': 'Unnamed BLE device with strong signal',
})
if not threats:
return None
# Return highest severity threat
threats.sort(key=lambda t: ['low', 'medium', 'high', 'critical'].index(t['severity']), reverse=True)
return {
'threat_type': threats[0]['type'],
'severity': threats[0]['severity'],
'source': 'bluetooth',
'identifier': mac,
'name': name or 'Unknown BLE Device',
'signal_strength': rssi,
'details': {
'all_threats': threats,
'manufacturer': manufacturer,
'device_type': device_type,
}
}
def analyze_rf_signal(self, signal: dict) -> dict | None:
"""
Analyze an RF signal for threats.
Args:
signal: RF signal dict with frequency, level, etc.
Returns:
Threat dict if threat detected, None otherwise
"""
frequency = signal.get('frequency', 0)
level = signal.get('level', signal.get('power', -100))
modulation = signal.get('modulation', '')
if not frequency:
return None
threats = []
freq_rounded = round(frequency, 1)
# Check if new frequency (not in baseline)
if self.baseline and freq_rounded not in self.baseline_rf_freqs:
risk, band_name = get_frequency_risk(frequency)
threats.append({
'type': 'unknown_signal',
'severity': risk,
'reason': f'New signal in {band_name}',
})
# Check frequency risk even without baseline
risk, band_name = get_frequency_risk(frequency)
if risk in ['high', 'critical']:
threats.append({
'type': 'unknown_signal',
'severity': risk,
'reason': f'Signal in high-risk band: {band_name}',
})
if not threats:
return None
# Return highest severity threat
threats.sort(key=lambda t: ['low', 'medium', 'high', 'critical'].index(t['severity']), reverse=True)
return {
'threat_type': threats[0]['type'],
'severity': threats[0]['severity'],
'source': 'rf',
'identifier': f'{frequency:.3f} MHz',
'name': f'RF Signal @ {frequency:.3f} MHz',
'signal_strength': level,
'frequency': frequency,
'details': {
'all_threats': threats,
'modulation': modulation,
'band_name': band_name,
}
}
def analyze_all(
self,
wifi_devices: list[dict] | None = None,
bt_devices: list[dict] | None = None,
rf_signals: list[dict] | None = None
) -> list[dict]:
"""
Analyze all provided devices and signals for threats.
Returns:
List of detected threats sorted by severity
"""
threats = []
if wifi_devices:
for device in wifi_devices:
threat = self.analyze_wifi_device(device)
if threat:
threats.append(threat)
if bt_devices:
for device in bt_devices:
threat = self.analyze_bt_device(device)
if threat:
threats.append(threat)
if rf_signals:
for signal in rf_signals:
threat = self.analyze_rf_signal(signal)
if threat:
threats.append(threat)
# Sort by severity (critical first)
severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}
threats.sort(key=lambda t: severity_order.get(t.get('severity', 'low'), 3))
return threats
def classify_device_threat(
source: str,
device: dict,
baseline: dict | None = None
) -> dict | None:
"""
Convenience function to classify a single device.
Args:
source: Device source ('wifi', 'bluetooth', 'rf')
device: Device data dict
baseline: Optional baseline for comparison
Returns:
Threat dict if threat detected, None otherwise
"""
detector = ThreatDetector(baseline)
if source == 'wifi':
return detector.analyze_wifi_device(device)
elif source == 'bluetooth':
return detector.analyze_bt_device(device)
elif source == 'rf':
return detector.analyze_rf_signal(device)
return None