Add TSCM device classification system

Classification levels:
- Green (Informational): Known devices in baseline, expected infrastructure
- Yellow (Needs Review): Unknown BLE devices, new WiFi APs, unidentified RF
- Red (High Interest): Persistent transmitters, audio-capable BLE, trackers,
  devices with repeat detections across scans

Features:
- Device history tracking for repeat detection (24-hour window)
- Audio-capable BLE detection (headphones, mics, speakers)
- Classification reasons shown under each device
- Color-coded indicators with visual styling
- Microphone badge for audio-capable BLE devices
This commit is contained in:
Smittix
2026-01-14 13:52:28 +00:00
parent c11c1200e2
commit f407a3cb54
3 changed files with 321 additions and 9 deletions
+16 -3
View File
@@ -915,6 +915,8 @@ def _run_sweep(
sev = threat.get('severity', 'low').lower()
if sev in severity_counts:
severity_counts[sev] += 1
# Classify device
classification = detector.classify_wifi_device(network)
# Send device to frontend
_emit_event('wifi_device', {
'bssid': bssid,
@@ -923,7 +925,9 @@ def _run_sweep(
'signal': network.get('power', ''),
'security': network.get('privacy', ''),
'is_threat': is_threat,
'is_new': True
'is_new': not classification.get('in_baseline', False),
'classification': classification.get('classification', 'review'),
'reasons': classification.get('reasons', []),
})
last_wifi_scan = current_time
except Exception as e:
@@ -947,6 +951,8 @@ def _run_sweep(
sev = threat.get('severity', 'low').lower()
if sev in severity_counts:
severity_counts[sev] += 1
# Classify device
classification = detector.classify_bt_device(device)
# Send device to frontend
_emit_event('bt_device', {
'mac': mac,
@@ -954,7 +960,10 @@ def _run_sweep(
'type': device.get('type', ''),
'rssi': device.get('rssi', ''),
'is_threat': is_threat,
'is_new': True
'is_new': not classification.get('in_baseline', False),
'classification': classification.get('classification', 'review'),
'reasons': classification.get('reasons', []),
'is_audio_capable': classification.get('is_audio_capable', False),
})
last_bt_scan = current_time
except Exception as e:
@@ -985,6 +994,8 @@ def _run_sweep(
sev = threat.get('severity', 'low').lower()
if sev in severity_counts:
severity_counts[sev] += 1
# Classify signal
classification = detector.classify_rf_signal(signal)
# Send signal to frontend
_emit_event('rf_signal', {
'frequency': signal['frequency'],
@@ -992,7 +1003,9 @@ def _run_sweep(
'band': signal['band'],
'signal_strength': signal.get('signal_strength', 0),
'is_threat': is_threat,
'is_new': True
'is_new': not classification.get('in_baseline', False),
'classification': classification.get('classification', 'review'),
'reasons': classification.get('reasons', []),
})
last_rf_scan = current_time
except Exception as e:
+65 -6
View File
@@ -2036,6 +2036,33 @@
.tscm-device-item.baseline {
border-left-color: #00ff88;
}
/* Classification colors */
.tscm-device-item.classification-green {
border-left-color: #00cc00;
background: rgba(0, 204, 0, 0.1);
}
.tscm-device-item.classification-yellow {
border-left-color: #ffcc00;
background: rgba(255, 204, 0, 0.1);
}
.tscm-device-item.classification-red {
border-left-color: #ff3333;
background: rgba(255, 51, 51, 0.15);
animation: pulse-glow 2s infinite;
}
.classification-indicator {
margin-right: 6px;
}
.tscm-device-reasons {
font-size: 9px;
color: var(--text-muted);
margin-top: 4px;
font-style: italic;
}
.audio-badge {
margin-left: 6px;
font-size: 10px;
}
.tscm-device-name {
font-weight: 600;
font-size: 12px;
@@ -9981,6 +10008,25 @@
document.getElementById('tscmThreatCount').textContent = tscmThreats.length;
}
function getClassificationClass(classification) {
// Map classification to CSS class
switch (classification) {
case 'high_interest': return 'classification-red';
case 'review': return 'classification-yellow';
case 'informational': return 'classification-green';
default: return 'classification-yellow';
}
}
function getClassificationIcon(classification) {
switch (classification) {
case 'high_interest': return '🔴';
case 'review': return '🟡';
case 'informational': return '🟢';
default: return '🟡';
}
}
function updateTscmDisplays() {
// Update WiFi list
const wifiList = document.getElementById('tscmWifiList');
@@ -9988,12 +10034,16 @@
wifiList.innerHTML = '<div class="tscm-empty">No WiFi networks detected</div>';
} else {
wifiList.innerHTML = tscmWifiDevices.map(d => `
<div class="tscm-device-item ${d.is_threat ? 'threat' : (d.is_new ? 'new' : 'baseline')}">
<div class="tscm-device-name">${escapeHtml(d.ssid || d.bssid || 'Hidden')}</div>
<div class="tscm-device-item ${getClassificationClass(d.classification)}">
<div class="tscm-device-name">
<span class="classification-indicator">${getClassificationIcon(d.classification)}</span>
${escapeHtml(d.ssid || d.bssid || 'Hidden')}
</div>
<div class="tscm-device-meta">
<span>${d.bssid}</span>
<span>${d.signal || '--'} dBm</span>
</div>
${d.reasons && d.reasons.length > 0 ? `<div class="tscm-device-reasons">${d.reasons.join(' • ')}</div>` : ''}
</div>
`).join('');
}
@@ -10005,12 +10055,17 @@
btList.innerHTML = '<div class="tscm-empty">No Bluetooth devices detected</div>';
} else {
btList.innerHTML = tscmBtDevices.map(d => `
<div class="tscm-device-item ${d.is_threat ? 'threat' : (d.is_new ? 'new' : 'baseline')}">
<div class="tscm-device-name">${escapeHtml(d.name || 'Unknown')}</div>
<div class="tscm-device-item ${getClassificationClass(d.classification)}">
<div class="tscm-device-name">
<span class="classification-indicator">${getClassificationIcon(d.classification)}</span>
${escapeHtml(d.name || 'Unknown')}
${d.is_audio_capable ? '<span class="audio-badge" title="Audio-capable device">🎤</span>' : ''}
</div>
<div class="tscm-device-meta">
<span>${d.mac}</span>
<span>${d.rssi || '--'} dBm</span>
</div>
${d.reasons && d.reasons.length > 0 ? `<div class="tscm-device-reasons">${d.reasons.join(' • ')}</div>` : ''}
</div>
`).join('');
}
@@ -10022,12 +10077,16 @@
rfList.innerHTML = '<div class="tscm-empty">No RF signals detected</div>';
} else {
rfList.innerHTML = tscmRfSignals.map(s => `
<div class="tscm-device-item ${s.is_threat ? 'threat' : (s.is_new ? 'new' : 'baseline')}">
<div class="tscm-device-name">${s.frequency.toFixed(3)} MHz</div>
<div class="tscm-device-item ${getClassificationClass(s.classification)}">
<div class="tscm-device-name">
<span class="classification-indicator">${getClassificationIcon(s.classification)}</span>
${s.frequency.toFixed(3)} MHz
</div>
<div class="tscm-device-meta">
<span>${s.band}</span>
<span>${s.power.toFixed(1)} dBm</span>
</div>
${s.reasons && s.reasons.length > 0 ? `<div class="tscm-device-reasons">${s.reasons.join(' • ')}</div>` : ''}
</div>
`).join('');
}
+240
View File
@@ -23,6 +23,68 @@ from data.tscm_frequencies import (
logger = logging.getLogger('intercept.tscm.detector')
# Classification levels for TSCM devices
CLASSIFICATION_LEVELS = {
'informational': {
'color': '#00cc00', # Green
'label': 'Informational',
'description': 'Known device, expected infrastructure, or background noise',
},
'review': {
'color': '#ffcc00', # Yellow
'label': 'Needs Review',
'description': 'Unknown device requiring investigation',
},
'high_interest': {
'color': '#ff3333', # Red
'label': 'High Interest',
'description': 'Suspicious device requiring immediate attention',
},
}
# BLE device types that can transmit audio (potential bugs)
AUDIO_CAPABLE_BLE_NAMES = [
'headphone', 'headset', 'earphone', 'earbud', 'speaker',
'audio', 'mic', 'microphone', 'airpod', 'buds',
'jabra', 'bose', 'sony wf', 'sony wh', 'beats',
'jbl', 'soundcore', 'anker', 'skullcandy',
]
# Device history for tracking repeat detections across scans
_device_history: dict[str, list[datetime]] = {}
_history_window_hours = 24 # Consider detections within 24 hours
def _record_device_seen(identifier: str) -> int:
"""Record a device sighting and return count of times seen."""
now = datetime.now()
if identifier not in _device_history:
_device_history[identifier] = []
# Clean old entries
cutoff = now.timestamp() - (_history_window_hours * 3600)
_device_history[identifier] = [
dt for dt in _device_history[identifier]
if dt.timestamp() > cutoff
]
_device_history[identifier].append(now)
return len(_device_history[identifier])
def _is_audio_capable_ble(name: str | None, device_type: str | None = None) -> bool:
"""Check if a BLE device might be audio-capable."""
if name:
name_lower = name.lower()
for pattern in AUDIO_CAPABLE_BLE_NAMES:
if pattern in name_lower:
return True
if device_type:
type_lower = device_type.lower()
if any(t in type_lower for t in ['audio', 'headset', 'headphone', 'speaker']):
return True
return False
class ThreatDetector:
"""
@@ -72,6 +134,184 @@ class ThreatDetector:
f"{len(self.baseline_bt_macs)} BT, {len(self.baseline_rf_freqs)} RF"
)
def classify_wifi_device(self, device: dict) -> dict:
"""
Classify a WiFi device into informational/review/high_interest.
Returns:
Dict with 'classification', 'reasons', and metadata
"""
mac = device.get('bssid', device.get('mac', '')).upper()
ssid = device.get('essid', device.get('ssid', ''))
signal = device.get('power', device.get('signal', -100))
reasons = []
classification = 'informational'
# Track repeat detections
times_seen = _record_device_seen(f'wifi:{mac}') if mac else 1
# Check if in baseline (known device)
in_baseline = mac in self.baseline_wifi_macs if self.baseline else False
if in_baseline:
reasons.append('Known device in baseline')
classification = 'informational'
else:
# New/unknown device
reasons.append('New WiFi access point')
classification = 'review'
# Check for suspicious patterns -> high interest
if is_potential_camera(ssid=ssid, mac=mac):
reasons.append('Matches camera device patterns')
classification = 'high_interest'
if not ssid and signal and int(signal) > -60:
reasons.append('Hidden SSID with strong signal')
classification = 'high_interest'
# Repeat detections across scans
if times_seen >= 3:
reasons.append(f'Repeat detection ({times_seen} times)')
if classification != 'high_interest':
classification = 'high_interest'
return {
'classification': classification,
'reasons': reasons,
'in_baseline': in_baseline,
'times_seen': times_seen,
}
def classify_bt_device(self, device: dict) -> dict:
"""
Classify a Bluetooth device into informational/review/high_interest.
Returns:
Dict with 'classification', 'reasons', and metadata
"""
mac = device.get('mac', device.get('address', '')).upper()
name = device.get('name', '')
rssi = device.get('rssi', device.get('signal', -100))
device_type = device.get('type', '')
manufacturer_data = device.get('manufacturer_data')
reasons = []
classification = 'informational'
tracker_info = None
# Track repeat detections
times_seen = _record_device_seen(f'bt:{mac}') if mac else 1
# Check if in baseline (known device)
in_baseline = mac in self.baseline_bt_macs if self.baseline else False
# Check for trackers (do this early for all devices)
tracker_info = is_known_tracker(name, manufacturer_data)
if in_baseline:
reasons.append('Known device in baseline')
classification = 'informational'
else:
# New/unknown BLE device
if not name or name == 'Unknown':
reasons.append('Unknown BLE device')
classification = 'review'
else:
reasons.append('New Bluetooth device')
classification = 'review'
# Check for trackers -> high interest
if tracker_info:
reasons.append(f"Known tracker: {tracker_info.get('name', 'Unknown')}")
classification = 'high_interest'
# Check for audio-capable devices -> high interest
if _is_audio_capable_ble(name, device_type):
reasons.append('Audio-capable BLE device')
classification = 'high_interest'
# Strong signal from unknown device
if rssi and int(rssi) > -50 and not name:
reasons.append('Strong signal from unnamed device')
classification = 'high_interest'
# Repeat detections across scans
if times_seen >= 3:
reasons.append(f'Repeat detection ({times_seen} times)')
if classification != 'high_interest':
classification = 'high_interest'
return {
'classification': classification,
'reasons': reasons,
'in_baseline': in_baseline,
'times_seen': times_seen,
'is_tracker': tracker_info is not None,
'is_audio_capable': _is_audio_capable_ble(name, device_type),
}
def classify_rf_signal(self, signal: dict) -> dict:
"""
Classify an RF signal into informational/review/high_interest.
Returns:
Dict with 'classification', 'reasons', and metadata
"""
frequency = signal.get('frequency', 0)
power = signal.get('power', signal.get('level', -100))
band = signal.get('band', '')
reasons = []
classification = 'informational'
freq_rounded = round(frequency, 1)
# Track repeat detections
times_seen = _record_device_seen(f'rf:{freq_rounded}')
# Check if in baseline (known frequency)
in_baseline = freq_rounded in self.baseline_rf_freqs if self.baseline else False
# Get frequency risk info
risk, band_name = get_frequency_risk(frequency)
if in_baseline:
reasons.append('Known frequency in baseline')
classification = 'informational'
else:
# New/unidentified RF carrier
reasons.append(f'Unidentified RF carrier in {band_name}')
if risk == 'low':
reasons.append('Background RF noise band')
classification = 'review'
elif risk == 'medium':
reasons.append('ISM band signal')
classification = 'review'
elif risk in ['high', 'critical']:
reasons.append(f'High-risk surveillance band: {band_name}')
classification = 'high_interest'
# Strong persistent signal
if power and float(power) > -40:
reasons.append('Strong persistent transmitter')
classification = 'high_interest'
# Repeat detections (persistent transmitter)
if times_seen >= 2:
reasons.append(f'Persistent transmitter ({times_seen} detections)')
classification = 'high_interest'
return {
'classification': classification,
'reasons': reasons,
'in_baseline': in_baseline,
'times_seen': times_seen,
'risk_level': risk,
'band_name': band_name,
}
def analyze_wifi_device(self, device: dict) -> dict | None:
"""
Analyze a WiFi device for threats.