""" TSCM Cross-Protocol Correlation Engine Correlates Bluetooth, Wi-Fi, and RF indicators to detect potential surveillance activity. Implements scoring model for risk assessment and provides actionable intelligence. DISCLAIMER: This system performs wireless and RF surveillance screening. Findings indicate anomalies and indicators, not confirmed surveillance devices. """ from __future__ import annotations import contextlib import logging from dataclasses import dataclass, field from datetime import datetime, timedelta from enum import Enum logger = logging.getLogger("intercept.tscm.correlation") class RiskLevel(Enum): """Risk classification levels.""" INFORMATIONAL = "informational" # Score 0-2 NEEDS_REVIEW = "needs_review" # Score 3-5 HIGH_INTEREST = "high_interest" # Score 6+ class IndicatorType(Enum): """Types of risk indicators.""" UNKNOWN_DEVICE = "unknown_device" AUDIO_CAPABLE = "audio_capable" PERSISTENT = "persistent" MEETING_CORRELATED = "meeting_correlated" CROSS_PROTOCOL = "cross_protocol" HIDDEN_IDENTITY = "hidden_identity" ROGUE_AP = "rogue_ap" BURST_TRANSMISSION = "burst_transmission" STABLE_RSSI = "stable_rssi" HIGH_FREQ_ADVERTISING = "high_freq_advertising" MAC_ROTATION = "mac_rotation" NARROWBAND_SIGNAL = "narrowband_signal" ALWAYS_ON_CARRIER = "always_on_carrier" # Tracker-specific indicators KNOWN_TRACKER = "known_tracker" AIRTAG_DETECTED = "airtag_detected" TILE_DETECTED = "tile_detected" SMARTTAG_DETECTED = "smarttag_detected" ESP32_DEVICE = "esp32_device" GENERIC_CHIPSET = "generic_chipset" # Scoring weights for each indicator INDICATOR_SCORES = { IndicatorType.UNKNOWN_DEVICE: 1, IndicatorType.AUDIO_CAPABLE: 2, IndicatorType.PERSISTENT: 2, IndicatorType.MEETING_CORRELATED: 2, IndicatorType.CROSS_PROTOCOL: 3, IndicatorType.HIDDEN_IDENTITY: 2, IndicatorType.ROGUE_AP: 3, IndicatorType.BURST_TRANSMISSION: 2, IndicatorType.STABLE_RSSI: 1, IndicatorType.HIGH_FREQ_ADVERTISING: 1, IndicatorType.MAC_ROTATION: 1, IndicatorType.NARROWBAND_SIGNAL: 2, IndicatorType.ALWAYS_ON_CARRIER: 2, # Tracker scores - higher for covert tracking devices IndicatorType.KNOWN_TRACKER: 3, IndicatorType.AIRTAG_DETECTED: 3, IndicatorType.TILE_DETECTED: 2, IndicatorType.SMARTTAG_DETECTED: 2, IndicatorType.ESP32_DEVICE: 2, IndicatorType.GENERIC_CHIPSET: 1, } # Known tracker device signatures TRACKER_SIGNATURES = { # Apple AirTag - OUI prefixes "airtag_oui": ["4C:E6:76", "7C:04:D0", "DC:A4:CA", "F0:B3:EC"], # Tile trackers "tile_oui": ["D0:03:DF", "EC:2E:4E"], # Samsung SmartTag "smarttag_oui": ["8C:71:F8", "CC:2D:83", "F0:5C:D5"], # ESP32/ESP8266 Espressif chipsets "espressif_oui": [ "24:0A:C4", "24:6F:28", "24:62:AB", "30:AE:A4", "3C:61:05", "3C:71:BF", "40:F5:20", "48:3F:DA", "4C:11:AE", "54:43:B2", "58:BF:25", "5C:CF:7F", "60:01:94", "68:C6:3A", "7C:9E:BD", "84:0D:8E", "84:CC:A8", "84:F3:EB", "8C:AA:B5", "90:38:0C", "94:B5:55", "98:CD:AC", "A4:7B:9D", "A4:CF:12", "AC:67:B2", "B4:E6:2D", "BC:DD:C2", "C4:4F:33", "C8:2B:96", "CC:50:E3", "D8:A0:1D", "DC:4F:22", "E0:98:06", "E8:68:E7", "EC:FA:BC", "F4:CF:A2", ], # Generic/suspicious chipset vendors (potential covert devices) "generic_chipset_oui": [ "00:1A:7D", # cyber-blue(HK) "00:25:00", # Apple (but generic BLE) ], } @dataclass class Indicator: """A single risk indicator.""" type: IndicatorType description: str score: int details: dict = field(default_factory=dict) timestamp: datetime = field(default_factory=datetime.now) @dataclass class DeviceProfile: """Complete profile for a detected device.""" # Identity identifier: str # MAC, BSSID, or frequency protocol: str # 'bluetooth', 'wifi', 'rf' # Device info name: str | None = None manufacturer: str | None = None device_type: str | None = None tracker_type: str | None = None tracker_name: str | None = None tracker_confidence: str | None = None tracker_confidence_score: float | None = None tracker_evidence: list[str] = field(default_factory=list) # Bluetooth-specific services: list[str] = field(default_factory=list) company_id: int | None = None advertising_interval: int | None = None # Wi-Fi-specific ssid: str | None = None channel: int | None = None encryption: str | None = None beacon_interval: int | None = None is_hidden: bool = False # RF-specific frequency: float | None = None bandwidth: float | None = None modulation: str | None = None # Common measurements rssi_samples: list[tuple[datetime, int]] = field(default_factory=list) first_seen: datetime | None = None last_seen: datetime | None = None detection_count: int = 0 # Behavioral analysis indicators: list[Indicator] = field(default_factory=list) total_score: int = 0 risk_level: RiskLevel = RiskLevel.INFORMATIONAL # Correlation correlated_devices: list[str] = field(default_factory=list) # Output confidence: float = 0.0 recommended_action: str = "monitor" known_device: bool = False known_device_name: str | None = None score_modifier: int = 0 def add_rssi_sample(self, rssi: int) -> None: """Add an RSSI sample with timestamp.""" self.rssi_samples.append((datetime.now(), rssi)) # Keep last 100 samples if len(self.rssi_samples) > 100: self.rssi_samples = self.rssi_samples[-100:] def get_rssi_stability(self) -> float: """Calculate RSSI stability (0-1, higher = more stable).""" if len(self.rssi_samples) < 3: return 0.0 values = [r for _, r in self.rssi_samples[-20:]] if not values: return 0.0 avg = sum(values) / len(values) variance = sum((v - avg) ** 2 for v in values) / len(values) # Convert variance to stability score (lower variance = higher stability) # Variance of ~0 = 1.0, variance of 100+ = ~0 return max(0, 1 - (variance / 100)) def add_indicator(self, indicator_type: IndicatorType, description: str, details: dict = None) -> None: """Add a risk indicator and update score.""" score = INDICATOR_SCORES.get(indicator_type, 1) self.indicators.append( Indicator(type=indicator_type, description=description, score=score, details=details or {}) ) self._recalculate_score() def _recalculate_score(self) -> None: """Recalculate total score and risk level.""" self.total_score = sum(i.score for i in self.indicators) if self.total_score >= 6: self.risk_level = RiskLevel.HIGH_INTEREST self.recommended_action = "investigate" elif self.total_score >= 3: self.risk_level = RiskLevel.NEEDS_REVIEW self.recommended_action = "review" else: self.risk_level = RiskLevel.INFORMATIONAL self.recommended_action = "monitor" # Calculate confidence based on number and quality of indicators indicator_count = len(self.indicators) self.confidence = min(1.0, (indicator_count * 0.15) + (self.total_score * 0.05)) def apply_score_modifier(self, modifier: int | None) -> None: """Apply a score modifier (e.g., known-good device adjustment).""" base_score = sum(i.score for i in self.indicators) modifier_val = int(modifier) if modifier is not None else 0 self.score_modifier = modifier_val self.total_score = max(0, base_score + modifier_val) if self.total_score >= 6: self.risk_level = RiskLevel.HIGH_INTEREST self.recommended_action = "investigate" elif self.total_score >= 3: self.risk_level = RiskLevel.NEEDS_REVIEW self.recommended_action = "review" else: self.risk_level = RiskLevel.INFORMATIONAL self.recommended_action = "monitor" indicator_count = len(self.indicators) self.confidence = min(1.0, (indicator_count * 0.15) + (self.total_score * 0.05)) def to_dict(self) -> dict: """Convert to dictionary for JSON serialization.""" return { "identifier": self.identifier, "protocol": self.protocol, "name": self.name, "manufacturer": self.manufacturer, "device_type": self.device_type, "tracker_type": self.tracker_type, "tracker_name": self.tracker_name, "tracker_confidence": self.tracker_confidence, "tracker_confidence_score": self.tracker_confidence_score, "tracker_evidence": self.tracker_evidence, "ssid": self.ssid, "frequency": self.frequency, "first_seen": self.first_seen.isoformat() if self.first_seen else None, "last_seen": self.last_seen.isoformat() if self.last_seen else None, "detection_count": self.detection_count, "rssi_current": self.rssi_samples[-1][1] if self.rssi_samples else None, "rssi_stability": self.get_rssi_stability(), "indicators": [ { "type": i.type.value, "description": i.description, "score": i.score, } for i in self.indicators ], "total_score": self.total_score, "score_modifier": self.score_modifier, "risk_level": self.risk_level.value, "confidence": round(self.confidence, 2), "recommended_action": self.recommended_action, "correlated_devices": self.correlated_devices, "known_device": self.known_device, "known_device_name": self.known_device_name, } # Known audio-capable BLE service UUIDs AUDIO_SERVICE_UUIDS = [ "0000110b-0000-1000-8000-00805f9b34fb", # A2DP Sink "0000110a-0000-1000-8000-00805f9b34fb", # A2DP Source "0000111e-0000-1000-8000-00805f9b34fb", # Handsfree "0000111f-0000-1000-8000-00805f9b34fb", # Handsfree Audio Gateway "00001108-0000-1000-8000-00805f9b34fb", # Headset "00001203-0000-1000-8000-00805f9b34fb", # Generic Audio ] _BT_BASE_UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb" def _normalize_bt_uuid(value: str) -> str: """Normalize BLE UUIDs to 16-bit where possible.""" if not value: return "" uuid = str(value).lower().strip() if uuid.startswith("0x"): uuid = uuid[2:] if uuid.endswith(_BT_BASE_UUID_SUFFIX) and len(uuid) >= 8: return uuid[4:8] if len(uuid) == 4: return uuid return uuid AUDIO_SERVICE_UUIDS_16 = {_normalize_bt_uuid(u) for u in AUDIO_SERVICE_UUIDS} # Generic chipset vendors (often used in covert devices) GENERIC_CHIPSET_VENDORS = [ "espressif", "nordic", "texas instruments", "silicon labs", "realtek", "mediatek", "qualcomm", "broadcom", "cypress", "dialog", ] # Suspicious frequency ranges for RF SUSPICIOUS_RF_BANDS = [ {"start": 136, "end": 174, "name": "VHF", "risk": "high"}, {"start": 400, "end": 470, "name": "UHF", "risk": "high"}, {"start": 315, "end": 316, "name": "315 MHz ISM", "risk": "medium"}, {"start": 433, "end": 435, "name": "433 MHz ISM", "risk": "medium"}, {"start": 868, "end": 870, "name": "868 MHz ISM", "risk": "medium"}, {"start": 902, "end": 928, "name": "915 MHz ISM", "risk": "medium"}, ] class CorrelationEngine: """ Cross-protocol correlation engine for TSCM analysis. Correlates Bluetooth, Wi-Fi, and RF indicators to identify potential surveillance activity patterns. """ def __init__(self): self.device_profiles: dict[str, DeviceProfile] = {} self.meeting_windows: list[tuple[datetime, datetime]] = [] self.correlation_window = timedelta(minutes=5) self._known_device_cache: dict[str, dict | None] = {} def start_meeting_window(self) -> None: """Mark the start of a sensitive period (meeting).""" self.meeting_windows.append((datetime.now(), None)) logger.info("Meeting window started") def end_meeting_window(self) -> None: """Mark the end of a sensitive period.""" if self.meeting_windows and self.meeting_windows[-1][1] is None: start = self.meeting_windows[-1][0] self.meeting_windows[-1] = (start, datetime.now()) logger.info("Meeting window ended") def is_during_meeting(self, timestamp: datetime = None) -> bool: """Check if timestamp falls within a meeting window.""" ts = timestamp or datetime.now() for start, end in self.meeting_windows: if end is None: if ts >= start: return True elif start <= ts <= end: return True return False def _lookup_known_device(self, identifier: str, protocol: str) -> dict | None: """Lookup known-good device details with light normalization.""" cache_key = f"{protocol}:{identifier}" if cache_key in self._known_device_cache: return self._known_device_cache[cache_key] try: from utils.database import is_known_good_device candidates = [] if identifier: candidates.append(str(identifier)) if protocol == "rf": try: freq_val = float(identifier) candidates.append(f"{freq_val:.3f}") candidates.append(f"{freq_val:.1f}") except (ValueError, TypeError): pass known = None for cand in candidates: if not cand: continue known = is_known_good_device(str(cand).upper()) if known: break except Exception: known = None self._known_device_cache[cache_key] = known return known def _apply_known_device_modifier(self, profile: DeviceProfile, identifier: str, protocol: str) -> None: """Apply known-good score modifier and update profile metadata.""" known = self._lookup_known_device(identifier, protocol) if known: profile.known_device = True profile.known_device_name = known.get("name") if isinstance(known, dict) else None modifier = known.get("score_modifier", 0) if isinstance(known, dict) else 0 else: profile.known_device = False profile.known_device_name = None modifier = 0 profile.apply_score_modifier(modifier) def get_or_create_profile(self, identifier: str, protocol: str) -> DeviceProfile: """Get existing profile or create new one.""" key = f"{protocol}:{identifier}" if key not in self.device_profiles: self.device_profiles[key] = DeviceProfile( identifier=identifier, protocol=protocol, first_seen=datetime.now() ) profile = self.device_profiles[key] profile.last_seen = datetime.now() profile.detection_count += 1 return profile def analyze_bluetooth_device(self, device: dict) -> DeviceProfile: """ Analyze a Bluetooth device for suspicious indicators. Args: device: Dict with mac, name, rssi, services, manufacturer, etc. Returns: DeviceProfile with risk assessment """ mac = device.get("mac", device.get("address", "")).upper() profile = self.get_or_create_profile(mac, "bluetooth") # Update profile data profile.name = device.get("name") or profile.name profile.manufacturer = device.get("manufacturer") or profile.manufacturer profile.device_type = device.get("type") or profile.device_type services = device.get("services") if not services: services = device.get("service_uuids") profile.services = services or profile.services profile.company_id = device.get("company_id") or profile.company_id profile.advertising_interval = device.get("advertising_interval") or profile.advertising_interval tracker_data = device.get("tracker") or {} if tracker_data: profile.tracker_type = tracker_data.get("type") or profile.tracker_type profile.tracker_name = tracker_data.get("name") or profile.tracker_name profile.tracker_confidence = tracker_data.get("confidence") or profile.tracker_confidence profile.tracker_confidence_score = tracker_data.get("confidence_score") or profile.tracker_confidence_score evidence = tracker_data.get("evidence") if isinstance(evidence, list): profile.tracker_evidence = evidence elif evidence: profile.tracker_evidence = [str(evidence)] # Add RSSI sample rssi = device.get("rssi", device.get("signal")) if rssi: with contextlib.suppress(ValueError, TypeError): profile.add_rssi_sample(int(rssi)) # Clear previous indicators for fresh analysis profile.indicators = [] # === Detection Logic === # 1. Unknown manufacturer or generic chipset if not profile.manufacturer and mac and not device.get("is_randomized_mac"): try: first_octet = int(mac.split(":")[0], 16) except (ValueError, IndexError): first_octet = None if first_octet is None or not (first_octet & 0x02): try: from data.oui import get_manufacturer vendor = get_manufacturer(mac) if vendor and vendor != "Unknown": profile.manufacturer = vendor except Exception: pass if not profile.manufacturer: profile.add_indicator(IndicatorType.UNKNOWN_DEVICE, "Unknown manufacturer", {"manufacturer": None}) elif any(v in profile.manufacturer.lower() for v in GENERIC_CHIPSET_VENDORS): profile.add_indicator( IndicatorType.UNKNOWN_DEVICE, f"Generic chipset vendor: {profile.manufacturer}", {"manufacturer": profile.manufacturer}, ) # 2. No human-readable name if not profile.name or profile.name in ["Unknown", "", "N/A"]: profile.add_indicator(IndicatorType.HIDDEN_IDENTITY, "No device name advertised", {"name": profile.name}) # 3. Audio-capable services if profile.services: normalized_services = {_normalize_bt_uuid(s) for s in profile.services if s} audio_services = [s for s in normalized_services if s in AUDIO_SERVICE_UUIDS_16] if audio_services: profile.add_indicator( IndicatorType.AUDIO_CAPABLE, "Audio-capable BLE services detected", {"services": audio_services} ) # Check name for audio keywords if profile.name: audio_keywords = [ "headphone", "headset", "earphone", "speaker", "mic", "audio", "airpod", "buds", "jabra", "bose", ] if any(k in profile.name.lower() for k in audio_keywords): profile.add_indicator( IndicatorType.AUDIO_CAPABLE, f"Audio device name: {profile.name}", {"name": profile.name} ) # 4. High-frequency advertising (< 100ms interval is suspicious) if profile.advertising_interval and profile.advertising_interval < 100: profile.add_indicator( IndicatorType.HIGH_FREQ_ADVERTISING, f"High advertising frequency: {profile.advertising_interval}ms", {"interval": profile.advertising_interval}, ) # 5. Persistent presence if profile.detection_count >= 3: profile.add_indicator( IndicatorType.PERSISTENT, f"Persistent device ({profile.detection_count} detections)", {"count": profile.detection_count}, ) # 6. Stable RSSI (suggests fixed placement) rssi_stability = profile.get_rssi_stability() if rssi_stability > 0.7 and len(profile.rssi_samples) >= 5: profile.add_indicator( IndicatorType.STABLE_RSSI, f"Stable signal strength (stability: {rssi_stability:.0%})", {"stability": rssi_stability}, ) # 7. Meeting correlation if self.is_during_meeting(): profile.add_indicator( IndicatorType.MEETING_CORRELATED, "Detected during sensitive period", {"during_meeting": True} ) # 8. MAC rotation pattern (random MAC prefix) if mac and mac[1] in ["2", "6", "A", "E", "a", "e"]: profile.add_indicator(IndicatorType.MAC_ROTATION, "Random/rotating MAC address detected", {"mac": mac}) # 9. Known tracker detection (AirTag, Tile, SmartTag, ESP32) mac_prefix = mac[:8] if len(mac) >= 8 else "" tracker_detected = False tracker_data = device.get("tracker") or {} if tracker_data.get("is_tracker"): tracker_detected = True tracker_label = tracker_data.get("name") or tracker_data.get("type") if tracker_label: label_lower = str(tracker_label).lower() if "airtag" in label_lower or "find my" in label_lower: profile.add_indicator( IndicatorType.AIRTAG_DETECTED, f"Tracker detected: {tracker_label}", {"mac": mac, "tracker_type": tracker_label}, ) profile.device_type = "AirTag" elif "tile" in label_lower: profile.add_indicator( IndicatorType.TILE_DETECTED, f"Tracker detected: {tracker_label}", {"mac": mac, "tracker_type": tracker_label}, ) profile.device_type = "Tile Tracker" elif "smarttag" in label_lower or "samsung" in label_lower: profile.add_indicator( IndicatorType.SMARTTAG_DETECTED, f"Tracker detected: {tracker_label}", {"mac": mac, "tracker_type": tracker_label}, ) profile.device_type = "Samsung SmartTag" else: profile.device_type = tracker_label elif not profile.device_type: profile.device_type = "Tracker" # Check for tracker flags from BLE scanner (manufacturer ID detection) if device.get("is_airtag"): profile.add_indicator( IndicatorType.AIRTAG_DETECTED, "Apple AirTag detected via manufacturer data", {"mac": mac, "tracker_type": "AirTag"}, ) profile.device_type = device.get("tracker_type", "AirTag") tracker_detected = True if device.get("is_tile"): profile.add_indicator( IndicatorType.TILE_DETECTED, "Tile tracker detected via manufacturer data", {"mac": mac, "tracker_type": "Tile"}, ) profile.device_type = "Tile Tracker" tracker_detected = True if device.get("is_smarttag"): profile.add_indicator( IndicatorType.SMARTTAG_DETECTED, "Samsung SmartTag detected via manufacturer data", {"mac": mac, "tracker_type": "SmartTag"}, ) profile.device_type = "Samsung SmartTag" tracker_detected = True if device.get("is_espressif"): profile.add_indicator( IndicatorType.ESP32_DEVICE, "ESP32/ESP8266 detected via Espressif manufacturer ID", {"mac": mac, "chipset": "Espressif"}, ) profile.manufacturer = "Espressif" profile.device_type = device.get("tracker_type", "ESP32/ESP8266") tracker_detected = True # Check manufacturer_id directly mfg_id = device.get("manufacturer_id") if mfg_id: if mfg_id == 0x004C and not device.get("is_airtag"): # Apple device - could be AirTag profile.manufacturer = "Apple" elif mfg_id == 0x02E5 and not device.get("is_espressif"): # Espressif device profile.add_indicator( IndicatorType.ESP32_DEVICE, "ESP32/ESP8266 detected via manufacturer ID", {"mac": mac, "manufacturer_id": mfg_id}, ) profile.manufacturer = "Espressif" tracker_detected = True # Fallback: Check for Apple AirTag by OUI if not tracker_detected and mac_prefix in TRACKER_SIGNATURES.get("airtag_oui", []): profile.add_indicator( IndicatorType.AIRTAG_DETECTED, "Apple AirTag detected - potential tracking device", {"mac": mac, "tracker_type": "AirTag"}, ) profile.device_type = "AirTag" tracker_detected = True # Check for Tile tracker if mac_prefix in TRACKER_SIGNATURES.get("tile_oui", []): profile.add_indicator( IndicatorType.TILE_DETECTED, "Tile tracker detected", {"mac": mac, "tracker_type": "Tile"} ) profile.device_type = "Tile Tracker" tracker_detected = True # Check for Samsung SmartTag if mac_prefix in TRACKER_SIGNATURES.get("smarttag_oui", []): profile.add_indicator( IndicatorType.SMARTTAG_DETECTED, "Samsung SmartTag detected", {"mac": mac, "tracker_type": "SmartTag"} ) profile.device_type = "Samsung SmartTag" tracker_detected = True # Check for ESP32/ESP8266 devices if mac_prefix in TRACKER_SIGNATURES.get("espressif_oui", []): profile.add_indicator( IndicatorType.ESP32_DEVICE, "ESP32/ESP8266 device detected - programmable hardware", {"mac": mac, "chipset": "Espressif"}, ) profile.manufacturer = "Espressif" tracker_detected = True # Check for generic/suspicious chipsets if mac_prefix in TRACKER_SIGNATURES.get("generic_chipset_oui", []): profile.add_indicator( IndicatorType.GENERIC_CHIPSET, "Generic chipset vendor - often used in covert devices", {"mac": mac} ) tracker_detected = True # If any tracker detected, add general tracker indicator if tracker_detected: profile.add_indicator(IndicatorType.KNOWN_TRACKER, "Known tracking device signature detected", {"mac": mac}) # Also check name for tracker keywords if profile.name: name_lower = profile.name.lower() if "airtag" in name_lower or "findmy" in name_lower: profile.add_indicator( IndicatorType.AIRTAG_DETECTED, f"AirTag identified by name: {profile.name}", {"name": profile.name} ) profile.device_type = "AirTag" elif "tile" in name_lower: profile.add_indicator( IndicatorType.TILE_DETECTED, f"Tile tracker identified by name: {profile.name}", {"name": profile.name}, ) profile.device_type = "Tile Tracker" elif "smarttag" in name_lower: profile.add_indicator( IndicatorType.SMARTTAG_DETECTED, f"SmartTag identified by name: {profile.name}", {"name": profile.name}, ) profile.device_type = "Samsung SmartTag" self._apply_known_device_modifier(profile, mac, "bluetooth") return profile def analyze_wifi_device(self, device: dict) -> DeviceProfile: """ Analyze a Wi-Fi device/AP for suspicious indicators. Args: device: Dict with bssid, ssid, channel, rssi, encryption, etc. Returns: DeviceProfile with risk assessment """ bssid = device.get("bssid", device.get("mac", "")).upper() profile = self.get_or_create_profile(bssid, "wifi") is_client = bool(device.get("is_client") or device.get("role") == "client") # Update profile data ssid = device.get("ssid", device.get("essid", "")) if is_client: profile.name = device.get("name") or device.get("vendor") or profile.name or f"Client ({bssid[-8:]})" profile.device_type = "client" profile.ssid = profile.ssid # Clients are not SSIDs profile.channel = device.get("channel") or profile.channel profile.encryption = profile.encryption profile.beacon_interval = profile.beacon_interval profile.is_hidden = False else: profile.ssid = ssid if ssid else profile.ssid profile.name = ssid or f"Hidden Network ({bssid[-8:]})" profile.channel = device.get("channel") or profile.channel profile.encryption = device.get("encryption", device.get("privacy")) or profile.encryption profile.beacon_interval = device.get("beacon_interval") or profile.beacon_interval profile.is_hidden = not ssid or ssid in ["", "Hidden", "[Hidden]"] # Extract manufacturer from OUI if bssid and len(bssid) >= 8: profile.manufacturer = device.get("vendor") or profile.manufacturer # Add RSSI sample rssi = device.get("rssi", device.get("power", device.get("signal"))) if rssi: with contextlib.suppress(ValueError, TypeError): profile.add_rssi_sample(int(rssi)) # Clear previous indicators profile.indicators = [] # === Detection Logic === if is_client: if not profile.manufacturer: profile.add_indicator(IndicatorType.UNKNOWN_DEVICE, "Unknown client manufacturer", {"mac": bssid}) if profile.detection_count >= 3: profile.add_indicator( IndicatorType.PERSISTENT, f"Persistent client ({profile.detection_count} detections)", {"count": profile.detection_count}, ) rssi_stability = profile.get_rssi_stability() if rssi_stability > 0.7 and len(profile.rssi_samples) >= 5: profile.add_indicator( IndicatorType.STABLE_RSSI, f"Stable client signal (stability: {rssi_stability:.0%})", {"stability": rssi_stability}, ) if self.is_during_meeting(): profile.add_indicator( IndicatorType.MEETING_CORRELATED, "Detected during sensitive period", {"during_meeting": True} ) try: first_octet = int(bssid.split(":")[0], 16) if first_octet & 0x02: profile.add_indicator( IndicatorType.MAC_ROTATION, "Random/locally administered MAC detected", {"mac": bssid} ) except (ValueError, IndexError): pass else: # 1. Hidden or unnamed SSID if profile.is_hidden: profile.add_indicator(IndicatorType.HIDDEN_IDENTITY, "Hidden or empty SSID", {"ssid": ssid}) # 2. BSSID not in authorized list (would need baseline) # For now, mark as unknown if no manufacturer if not profile.manufacturer: profile.add_indicator(IndicatorType.UNKNOWN_DEVICE, "Unknown AP manufacturer", {"bssid": bssid}) # 3. Consumer device OUI in restricted environment consumer_ouis = ["tp-link", "netgear", "d-link", "linksys", "asus"] if profile.manufacturer and any(c in profile.manufacturer.lower() for c in consumer_ouis): profile.add_indicator( IndicatorType.ROGUE_AP, f"Consumer-grade AP detected: {profile.manufacturer}", {"manufacturer": profile.manufacturer}, ) # 4. Camera device patterns camera_keywords = [ "cam", "camera", "ipcam", "dvr", "nvr", "wyze", "ring", "arlo", "nest", "blink", "eufy", "yi", ] if ssid and any(k in ssid.lower() for k in camera_keywords): profile.add_indicator( IndicatorType.AUDIO_CAPABLE, # Cameras often have mics f"Potential camera device: {ssid}", {"ssid": ssid}, ) # 5. Persistent presence if profile.detection_count >= 3: profile.add_indicator( IndicatorType.PERSISTENT, f"Persistent AP ({profile.detection_count} detections)", {"count": profile.detection_count}, ) # 6. Stable RSSI (fixed placement) rssi_stability = profile.get_rssi_stability() if rssi_stability > 0.7 and len(profile.rssi_samples) >= 5: profile.add_indicator( IndicatorType.STABLE_RSSI, f"Stable signal (stability: {rssi_stability:.0%})", {"stability": rssi_stability}, ) # 7. Meeting correlation if self.is_during_meeting(): profile.add_indicator( IndicatorType.MEETING_CORRELATED, "Detected during sensitive period", {"during_meeting": True} ) # 8. Strong hidden AP (very suspicious) if profile.is_hidden and profile.rssi_samples: latest_rssi = profile.rssi_samples[-1][1] if latest_rssi > -50: profile.add_indicator( IndicatorType.ROGUE_AP, f"Strong hidden AP (RSSI: {latest_rssi} dBm)", {"rssi": latest_rssi} ) self._apply_known_device_modifier(profile, bssid, "wifi") return profile def analyze_rf_signal(self, signal: dict) -> DeviceProfile: """ Analyze an RF signal for suspicious indicators. Args: signal: Dict with frequency, power, bandwidth, modulation, etc. Returns: DeviceProfile with risk assessment """ frequency = signal.get("frequency", 0) freq_key = f"{frequency:.3f}" profile = self.get_or_create_profile(freq_key, "rf") # Update profile data profile.frequency = frequency profile.name = f"{frequency:.3f} MHz" profile.bandwidth = signal.get("bandwidth") or profile.bandwidth profile.modulation = signal.get("modulation") or profile.modulation # Add power sample power = signal.get("power", signal.get("level")) if power: with contextlib.suppress(ValueError, TypeError): profile.add_rssi_sample(int(float(power))) # Clear previous indicators profile.indicators = [] # === Detection Logic === # 1. Determine frequency band risk band_info = None for band in SUSPICIOUS_RF_BANDS: if band["start"] <= frequency <= band["end"]: band_info = band break if band_info: if band_info["risk"] == "high": profile.add_indicator( IndicatorType.NARROWBAND_SIGNAL, f"Signal in high-risk band: {band_info['name']}", {"band": band_info["name"], "frequency": frequency}, ) else: profile.add_indicator( IndicatorType.UNKNOWN_DEVICE, f"Signal in ISM band: {band_info['name']}", {"band": band_info["name"], "frequency": frequency}, ) # 2. Narrowband FM/AM (potential bug) if profile.modulation and profile.modulation.lower() in ["fm", "nfm", "am"]: profile.add_indicator( IndicatorType.NARROWBAND_SIGNAL, f"Narrowband {profile.modulation.upper()} signal", {"modulation": profile.modulation}, ) # 3. Persistent/always-on carrier if profile.detection_count >= 2: profile.add_indicator( IndicatorType.ALWAYS_ON_CARRIER, f"Persistent carrier ({profile.detection_count} detections)", {"count": profile.detection_count}, ) # 4. Strong signal (close proximity) if profile.rssi_samples: latest_power = profile.rssi_samples[-1][1] if latest_power > -40: profile.add_indicator( IndicatorType.STABLE_RSSI, f"Strong signal suggesting close proximity ({latest_power} dBm)", {"power": latest_power}, ) # 5. Meeting correlation if self.is_during_meeting(): profile.add_indicator( IndicatorType.MEETING_CORRELATED, "Signal detected during sensitive period", {"during_meeting": True} ) self._apply_known_device_modifier(profile, freq_key, "rf") return profile def correlate_devices(self) -> list[dict]: """ Perform cross-protocol correlation analysis. Identifies devices across protocols that may be related. Returns: List of correlation findings """ correlations = [] now = datetime.now() # Get recent devices by protocol bt_devices = [ p for p in self.device_profiles.values() if p.protocol == "bluetooth" and p.last_seen and (now - p.last_seen) < self.correlation_window ] wifi_devices = [ p for p in self.device_profiles.values() if p.protocol == "wifi" and p.last_seen and (now - p.last_seen) < self.correlation_window ] rf_signals = [ p for p in self.device_profiles.values() if p.protocol == "rf" and p.last_seen and (now - p.last_seen) < self.correlation_window ] # Correlation 1: BLE audio device + RF narrowband signal audio_bt = [p for p in bt_devices if any(i.type == IndicatorType.AUDIO_CAPABLE for i in p.indicators)] narrowband_rf = [p for p in rf_signals if any(i.type == IndicatorType.NARROWBAND_SIGNAL for i in p.indicators)] for bt in audio_bt: for rf in narrowband_rf: correlation = { "type": "bt_audio_rf_narrowband", "description": "Audio-capable BLE device detected alongside narrowband RF signal", "devices": [bt.identifier, rf.identifier], "protocols": ["bluetooth", "rf"], "score_boost": 3, "significance": "high", } correlations.append(correlation) # Add cross-protocol indicator to both bt.add_indicator( IndicatorType.CROSS_PROTOCOL, f"Correlated with RF signal at {rf.frequency:.3f} MHz", {"correlated_device": rf.identifier}, ) rf.add_indicator( IndicatorType.CROSS_PROTOCOL, f"Correlated with BLE device {bt.identifier}", {"correlated_device": bt.identifier}, ) bt.correlated_devices.append(rf.identifier) rf.correlated_devices.append(bt.identifier) # Correlation 2: Rogue WiFi AP + RF burst activity rogue_aps = [p for p in wifi_devices if any(i.type == IndicatorType.ROGUE_AP for i in p.indicators)] rf_bursts = [ p for p in rf_signals if any(i.type in [IndicatorType.BURST_TRANSMISSION, IndicatorType.ALWAYS_ON_CARRIER] for i in p.indicators) ] for ap in rogue_aps: for rf in rf_bursts: correlation = { "type": "rogue_ap_rf_burst", "description": "Rogue AP detected alongside RF transmission", "devices": [ap.identifier, rf.identifier], "protocols": ["wifi", "rf"], "score_boost": 3, "significance": "high", } correlations.append(correlation) ap.add_indicator( IndicatorType.CROSS_PROTOCOL, f"Correlated with RF at {rf.frequency:.3f} MHz", {"correlated_device": rf.identifier}, ) rf.add_indicator( IndicatorType.CROSS_PROTOCOL, f"Correlated with AP {ap.ssid or ap.identifier}", {"correlated_device": ap.identifier}, ) # Correlation 3: Same vendor BLE + WiFi for bt in bt_devices: if bt.manufacturer: for wifi in wifi_devices: if wifi.manufacturer and bt.manufacturer.lower() in wifi.manufacturer.lower(): correlation = { "type": "same_vendor_bt_wifi", "description": f"Same vendor ({bt.manufacturer}) on BLE and WiFi", "devices": [bt.identifier, wifi.identifier], "protocols": ["bluetooth", "wifi"], "score_boost": 2, "significance": "medium", } correlations.append(correlation) # Re-apply known-good modifiers after correlation boosts for profile in self.device_profiles.values(): self._apply_known_device_modifier(profile, profile.identifier, profile.protocol) return correlations def get_high_interest_devices(self) -> list[DeviceProfile]: """Get all devices classified as high interest.""" return [p for p in self.device_profiles.values() if p.risk_level == RiskLevel.HIGH_INTEREST] def get_all_findings(self) -> dict: """ Get comprehensive findings report. Returns: Dict with all device profiles, correlations, and summary """ correlations = self.correlate_devices() devices_by_risk = { "high_interest": [], "needs_review": [], "informational": [], } for profile in self.device_profiles.values(): devices_by_risk[profile.risk_level.value].append(profile.to_dict()) return { "timestamp": datetime.now().isoformat(), "summary": { "total_devices": len(self.device_profiles), "high_interest": len(devices_by_risk["high_interest"]), "needs_review": len(devices_by_risk["needs_review"]), "informational": len(devices_by_risk["informational"]), "correlations_found": len(correlations), }, "devices": devices_by_risk, "correlations": correlations, "disclaimer": ( "This system performs wireless and RF surveillance screening. " "Findings indicate anomalies and indicators, not confirmed surveillance devices." ), } def clear_old_profiles(self, max_age_hours: int = 24) -> int: """Remove profiles older than specified age.""" cutoff = datetime.now() - timedelta(hours=max_age_hours) old_keys = [k for k, v in self.device_profiles.items() if v.last_seen and v.last_seen < cutoff] for key in old_keys: del self.device_profiles[key] return len(old_keys) # Global correlation engine instance _correlation_engine: CorrelationEngine | None = None def get_correlation_engine() -> CorrelationEngine: """Get or create the global correlation engine.""" global _correlation_engine if _correlation_engine is None: _correlation_engine = CorrelationEngine() return _correlation_engine def reset_correlation_engine() -> None: """Reset the global correlation engine.""" global _correlation_engine _correlation_engine = CorrelationEngine()