diff --git a/routes/tscm.py b/routes/tscm.py index 42b3085..e550624 100644 --- a/routes/tscm.py +++ b/routes/tscm.py @@ -647,40 +647,117 @@ def _scan_wifi_networks(interface: str) -> list[dict]: logger.warning(f"macOS WiFi scan failed: {e}") else: - # Linux: Try iwlist scan - iface = interface or 'wlan0' - try: - result = subprocess.run( - ['iwlist', iface, 'scan'], - capture_output=True, text=True, timeout=30 - ) - current_network = {} - for line in result.stdout.split('\n'): - line = line.strip() - if 'Cell' in line and 'Address:' in line: + # Linux: Try multiple scan methods + import shutil + + # Detect wireless interface if not specified + if not interface: + try: + import glob + wireless_paths = glob.glob('/sys/class/net/*/wireless') + if wireless_paths: + iface = wireless_paths[0].split('/')[4] + else: + iface = 'wlan0' + except Exception: + iface = 'wlan0' + else: + iface = interface + + logger.info(f"WiFi scan using interface: {iface}") + + # Method 1: Try iw scan (sometimes works without root) + if shutil.which('iw'): + try: + logger.info("Trying 'iw' scan...") + result = subprocess.run( + ['iw', 'dev', iface, 'scan'], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0 and 'BSS' in result.stdout: + # Parse iw output + current_bss = None + for line in result.stdout.split('\n'): + if line.startswith('BSS '): + if current_bss and current_bss.get('bssid'): + networks.append(current_bss) + # Extract BSSID from "BSS xx:xx:xx:xx:xx:xx(on wlan0)" + bssid_match = re.search(r'BSS ([0-9a-fA-F:]{17})', line) + if bssid_match: + current_bss = {'bssid': bssid_match.group(1).upper(), 'essid': '[Hidden]'} + elif current_bss: + line = line.strip() + if line.startswith('SSID:'): + ssid = line[5:].strip() + current_bss['essid'] = ssid or '[Hidden]' + elif line.startswith('signal:'): + sig_match = re.search(r'(-?\d+)', line) + if sig_match: + current_bss['power'] = sig_match.group(1) + elif line.startswith('freq:'): + freq = line[5:].strip() + # Convert frequency to channel + try: + freq_mhz = int(freq) + if freq_mhz < 3000: + channel = (freq_mhz - 2407) // 5 + else: + channel = (freq_mhz - 5000) // 5 + current_bss['channel'] = str(channel) + except ValueError: + pass + elif 'WPA' in line or 'RSN' in line: + current_bss['privacy'] = 'WPA2' if 'RSN' in line else 'WPA' + if current_bss and current_bss.get('bssid'): + networks.append(current_bss) + logger.info(f"iw scan found {len(networks)} networks") + elif 'Operation not permitted' in result.stderr or result.returncode != 0: + logger.warning(f"iw scan requires root: {result.stderr[:100]}") + except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e: + logger.warning(f"iw scan failed: {e}") + + # Method 2: Try iwlist scan if iw didn't work + if not networks and shutil.which('iwlist'): + try: + logger.info("Trying 'iwlist' scan...") + result = subprocess.run( + ['iwlist', iface, 'scan'], + capture_output=True, text=True, timeout=30 + ) + if 'Operation not permitted' in result.stderr: + logger.warning("iwlist scan requires root privileges") + else: + current_network = {} + for line in result.stdout.split('\n'): + line = line.strip() + if 'Cell' in line and 'Address:' in line: + if current_network.get('bssid'): + networks.append(current_network) + bssid = line.split('Address:')[1].strip() + current_network = {'bssid': bssid.upper(), 'essid': '[Hidden]'} + elif 'ESSID:' in line: + essid = line.split('ESSID:')[1].strip().strip('"') + current_network['essid'] = essid or '[Hidden]' + elif 'Channel:' in line: + channel = line.split('Channel:')[1].strip() + current_network['channel'] = channel + elif 'Signal level=' in line: + match = re.search(r'Signal level[=:]?\s*(-?\d+)', line) + if match: + current_network['power'] = match.group(1) + elif 'Encryption key:' in line: + encrypted = 'on' in line.lower() + current_network['encrypted'] = encrypted + elif 'WPA' in line or 'WPA2' in line: + current_network['privacy'] = 'WPA2' if 'WPA2' in line else 'WPA' if current_network.get('bssid'): networks.append(current_network) - bssid = line.split('Address:')[1].strip() - current_network = {'bssid': bssid.upper(), 'essid': '[Hidden]'} - elif 'ESSID:' in line: - essid = line.split('ESSID:')[1].strip().strip('"') - current_network['essid'] = essid or '[Hidden]' - elif 'Channel:' in line: - channel = line.split('Channel:')[1].strip() - current_network['channel'] = channel - elif 'Signal level=' in line: - match = re.search(r'Signal level[=:]?\s*(-?\d+)', line) - if match: - current_network['power'] = match.group(1) - elif 'Encryption key:' in line: - encrypted = 'on' in line.lower() - current_network['encrypted'] = encrypted - elif 'WPA' in line or 'WPA2' in line: - current_network['privacy'] = 'WPA2' if 'WPA2' in line else 'WPA' - if current_network.get('bssid'): - networks.append(current_network) - except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError) as e: - logger.warning(f"Linux WiFi scan failed: {e}") + logger.info(f"iwlist scan found {len(networks)} networks") + except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError) as e: + logger.warning(f"iwlist scan failed: {e}") + + if not networks: + logger.warning("WiFi scanning requires root privileges. Run with sudo for WiFi scanning.") return networks diff --git a/templates/index.html b/templates/index.html index 1eb006e..9d28c2c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2368,23 +2368,23 @@ No content is intercepted or decoded. Professional verification required. - +
-
- 0 - Critical +
+ 0 + High Interest
-
- 0 - High +
+ 0 + Needs Review
-
- 0 - Medium +
+ 0 + Informational
-
- 0 - Low +
+ 0 + Correlations
@@ -10136,24 +10136,29 @@ } function updateTscmThreatCounts() { - const counts = { critical: 0, high: 0, medium: 0, low: 0 }; - tscmThreats.forEach(t => { - if (counts[t.severity] !== undefined) { - counts[t.severity]++; - } + // Count devices by new scoring model classification + const counts = { high_interest: 0, review: 0, informational: 0 }; + + // Count from all device lists + [...tscmWifiDevices, ...tscmBtDevices, ...tscmRfSignals].forEach(d => { + const classification = d.classification || 'review'; + if (classification === 'high_interest') counts.high_interest++; + else if (classification === 'review') counts.review++; + else counts.informational++; }); - 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('tscmHighInterestCount').textContent = counts.high_interest; + document.getElementById('tscmNeedsReviewCount').textContent = counts.review; + document.getElementById('tscmInformationalCount').textContent = counts.informational; + document.getElementById('tscmCorrelationsCount').textContent = tscmCorrelations.length; - 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('tscmHighInterestCard').classList.toggle('active', counts.high_interest > 0); + document.getElementById('tscmNeedsReviewCard').classList.toggle('active', counts.review > 0); + document.getElementById('tscmInformationalCard').classList.toggle('active', counts.informational > 0); + document.getElementById('tscmCorrelationsCard').classList.toggle('active', tscmCorrelations.length > 0); - document.getElementById('tscmThreatCount').textContent = tscmThreats.length; + // Update threat panel count (now shows high interest items) + document.getElementById('tscmThreatCount').textContent = counts.high_interest; } function getClassificationClass(classification) { diff --git a/utils/tscm/correlation.py b/utils/tscm/correlation.py index 8562554..ce09007 100644 --- a/utils/tscm/correlation.py +++ b/utils/tscm/correlation.py @@ -41,6 +41,13 @@ class IndicatorType(Enum): 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 @@ -58,6 +65,39 @@ INDICATOR_SCORES = { 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) + ], } @@ -403,6 +443,92 @@ class CorrelationEngine: {'mac': mac} ) + # 9. Known tracker detection (AirTag, Tile, SmartTag, ESP32) + mac_prefix = mac[:8] if len(mac) >= 8 else '' + tracker_detected = False + + # Check for Apple AirTag + if 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' + return profile def analyze_wifi_device(self, device: dict) -> DeviceProfile: