""" 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 utils.database import ( create_tscm_baseline, get_active_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.wifi_clients: dict[str, dict] = {} # MAC -> client 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.wifi_clients = {} 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()) wifi_client_list = list(self.wifi_clients.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, wifi_clients=wifi_client_list, bt_devices=bt_list, rf_frequencies=rf_list, ) summary = { "baseline_id": self.current_baseline_id, "wifi_count": len(wifi_list), "wifi_client_count": len(wifi_client_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" ) 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_wifi_client(self, client: dict) -> None: """Add a WiFi client to the current baseline.""" if not self.recording: return mac = client.get("mac", client.get("address", "")).upper() if not mac: return if mac in self.wifi_clients: self.wifi_clients[mac].update( { "last_seen": datetime.now().isoformat(), "rssi": client.get("rssi", self.wifi_clients[mac].get("rssi")), "associated_bssid": client.get("associated_bssid", self.wifi_clients[mac].get("associated_bssid")), } ) else: self.wifi_clients[mac] = { "mac": mac, "vendor": client.get("vendor", ""), "rssi": client.get("rssi"), "associated_bssid": client.get("associated_bssid"), "probed_ssids": client.get("probed_ssids", []), "probe_count": client.get("probe_count", len(client.get("probed_ssids", []))), "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), "wifi_client_count": len(self.wifi_clients), "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_wifi_clients = { d.get("mac", d.get("address", "")).upper(): d for d in baseline.get("wifi_clients", []) 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_wifi_clients(self, current_devices: list[dict]) -> dict: """Compare current WiFi clients 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_wifi_clients: new_devices.append(device) else: matching_devices.append(device) for mac, device in self.baseline_wifi_clients.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, wifi_clients: 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, "wifi_clients": 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 wifi_clients is not None: results["wifi_clients"] = self.compare_wifi_clients(wifi_clients) results["total_new"] += results["wifi_clients"]["new_count"] results["total_missing"] += results["wifi_clients"]["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, wifi_clients: 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, wifi_clients, bt_devices, rf_signals)