mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 22:59:59 -07:00
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>
389 lines
13 KiB
Python
389 lines
13 KiB
Python
"""
|
|
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)
|