Update TSCM with improved WiFi scanning, new scoring UI, and tracker detection

WiFi Scanning:
- Add 'iw' scan method as primary (sometimes works without root)
- Auto-detect wireless interface from /sys/class/net
- Better error logging for permission issues
- Fall back to iwlist if iw fails

UI Updates:
- Replace Critical/High/Medium/Low cards with new scoring model
- Now shows: High Interest (6+), Needs Review (3-5), Informational (0-2)
- Add Correlations count card
- Update counts based on device classification scores

Tracker Detection:
- Add detection for Apple AirTag (by OUI and name)
- Add detection for Tile trackers
- Add detection for Samsung SmartTag
- Add detection for ESP32/ESP8266 devices (Espressif chipset)
- Add generic chipset vendor detection
- New indicator types with appropriate scoring weights

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-14 14:28:54 +00:00
parent b15b5ad9ba
commit 93b763865b
3 changed files with 267 additions and 59 deletions
+109 -32
View File
@@ -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
+32 -27
View File
@@ -2368,23 +2368,23 @@
No content is intercepted or decoded. Professional verification required.
</div>
<!-- Threat Summary Banner -->
<!-- Risk Summary Banner (new scoring model) -->
<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 class="threat-card critical" id="tscmHighInterestCard">
<span class="count" id="tscmHighInterestCount">0</span>
<span class="label">High Interest</span>
</div>
<div class="threat-card high" id="tscmHighCard">
<span class="count" id="tscmHighCount">0</span>
<span class="label">High</span>
<div class="threat-card high" id="tscmNeedsReviewCard">
<span class="count" id="tscmNeedsReviewCount">0</span>
<span class="label">Needs Review</span>
</div>
<div class="threat-card medium" id="tscmMediumCard">
<span class="count" id="tscmMediumCount">0</span>
<span class="label">Medium</span>
<div class="threat-card low" id="tscmInformationalCard">
<span class="count" id="tscmInformationalCount">0</span>
<span class="label">Informational</span>
</div>
<div class="threat-card low" id="tscmLowCard">
<span class="count" id="tscmLowCount">0</span>
<span class="label">Low</span>
<div class="threat-card medium" id="tscmCorrelationsCard">
<span class="count" id="tscmCorrelationsCount">0</span>
<span class="label">Correlations</span>
</div>
</div>
@@ -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) {
+126
View File
@@ -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: