mirror of
https://github.com/smittix/intercept.git
synced 2026-06-10 23:13:31 -07:00
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:
+109
-32
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user