mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
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:
4
app.py
4
app.py
@@ -114,6 +114,10 @@ aprs_rtl_process = None
|
||||
aprs_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||
aprs_lock = threading.Lock()
|
||||
|
||||
# TSCM (Technical Surveillance Countermeasures)
|
||||
tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||
tscm_lock = threading.Lock()
|
||||
|
||||
# ============================================
|
||||
# GLOBAL STATE DICTIONARIES
|
||||
# ============================================
|
||||
|
||||
436
data/tscm_frequencies.py
Normal file
436
data/tscm_frequencies.py
Normal 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
|
||||
@@ -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
639
routes/tscm.py
Normal 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'})
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
10
utils/tscm/__init__.py
Normal 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
388
utils/tscm/baseline.py
Normal 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
324
utils/tscm/detector.py
Normal 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
|
||||
Reference in New Issue
Block a user