diff --git a/intercept.py b/intercept.py index bcf49f3..df617e4 100755 --- a/intercept.py +++ b/intercept.py @@ -18,15 +18,101 @@ app = Flask(__name__) # Global process management current_process = None sensor_process = None +wifi_process = None +kismet_process = None +bt_process = None output_queue = queue.Queue() sensor_queue = queue.Queue() +wifi_queue = queue.Queue() +bt_queue = queue.Queue() process_lock = threading.Lock() sensor_lock = threading.Lock() +wifi_lock = threading.Lock() +bt_lock = threading.Lock() # Logging settings logging_enabled = False log_file_path = 'pager_messages.log' +# WiFi state +wifi_monitor_interface = None +wifi_networks = {} # BSSID -> network info +wifi_clients = {} # Client MAC -> client info +wifi_handshakes = [] # Captured handshakes + +# Bluetooth state +bt_interface = None +bt_devices = {} # MAC -> device info +bt_beacons = {} # MAC -> beacon info (AirTags, Tiles, iBeacons) +bt_services = {} # MAC -> list of services + +# Known beacon prefixes for detection +AIRTAG_PREFIXES = ['4C:00'] # Apple continuity +TILE_PREFIXES = ['C4:E7', 'DC:54', 'E4:B0', 'F8:8A'] +SAMSUNG_TRACKER = ['58:4D', 'A0:75'] + +# OUI Database for manufacturer lookup (common ones) +OUI_DATABASE = { + '00:00:0A': 'Omron', + '00:1A:7D': 'Cyber-Blue', + '00:1E:3D': 'Alps Electric', + '00:1F:20': 'Logitech', + '00:25:DB': 'Apple', + '04:52:F3': 'Apple', + '0C:3E:9F': 'Apple', + '10:94:BB': 'Apple', + '14:99:E2': 'Apple', + '20:78:F0': 'Apple', + '28:6A:BA': 'Apple', + '3C:22:FB': 'Apple', + '40:98:AD': 'Apple', + '48:D7:05': 'Apple', + '4C:57:CA': 'Apple', + '54:4E:90': 'Apple', + '5C:97:F3': 'Apple', + '60:F8:1D': 'Apple', + '68:DB:CA': 'Apple', + '70:56:81': 'Apple', + '78:7B:8A': 'Apple', + '7C:D1:C3': 'Apple', + '84:FC:FE': 'Apple', + '8C:2D:AA': 'Apple', + '90:B0:ED': 'Apple', + '98:01:A7': 'Apple', + '98:D6:BB': 'Apple', + 'A4:D1:D2': 'Apple', + 'AC:BC:32': 'Apple', + 'B0:34:95': 'Apple', + 'B8:C1:11': 'Apple', + 'C8:69:CD': 'Apple', + 'D0:03:4B': 'Apple', + 'DC:A9:04': 'Apple', + 'E0:C7:67': 'Apple', + 'F0:18:98': 'Apple', + 'F4:5C:89': 'Apple', + '00:1B:66': 'Samsung', + '00:21:19': 'Samsung', + '00:26:37': 'Samsung', + '5C:0A:5B': 'Samsung', + '8C:71:F8': 'Samsung', + 'C4:73:1E': 'Samsung', + '38:2C:4A': 'Samsung', + '00:1E:4C': 'Samsung', + '64:B5:C6': 'Liteon/Google', + '54:60:09': 'Google', + '00:1A:11': 'Google', + 'F4:F5:D8': 'Google', + '94:EB:2C': 'Google', + '78:4F:43': 'Apple', + 'F8:E4:E3': 'Tile', + 'C4:E7:BE': 'Tile', + 'E0:E5:CF': 'Raspberry Pi', + 'B8:27:EB': 'Raspberry Pi', + 'DC:A6:32': 'Raspberry Pi', + '00:0B:57': 'Silicon Wave', # BT Chips + '00:02:72': 'CC&C', # BT dongles +} + HTML_TEMPLATE = ''' @@ -734,6 +820,386 @@ HTML_TEMPLATE = ''' color: var(--accent-cyan); } + /* Recon Dashboard - Prominent Device Intelligence */ + .recon-panel { + background: var(--bg-card); + border: 1px solid var(--border-color); + margin: 15px; + margin-bottom: 10px; + position: relative; + } + + .recon-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, var(--accent-orange), var(--accent-cyan), transparent); + } + + .recon-panel.collapsed .recon-content { + display: none; + } + + .recon-header { + padding: 12px 15px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + } + + .recon-header h4 { + color: var(--accent-orange); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 2px; + margin: 0; + } + + .recon-stats { + display: flex; + gap: 15px; + font-size: 10px; + font-family: 'JetBrains Mono', monospace; + } + + .recon-stats span { + color: var(--accent-cyan); + } + + .recon-content { + max-height: 300px; + overflow-y: auto; + } + + .device-row { + display: grid; + grid-template-columns: 1fr auto auto auto; + gap: 10px; + padding: 10px 15px; + border-bottom: 1px solid var(--border-color); + font-size: 11px; + align-items: center; + transition: background 0.2s ease; + } + + .device-row:hover { + background: var(--bg-secondary); + } + + .device-row.anomaly { + border-left: 3px solid var(--accent-red); + background: rgba(255, 51, 102, 0.05); + } + + .device-row.new-device { + border-left: 3px solid var(--accent-green); + background: rgba(0, 255, 136, 0.05); + } + + .device-info { + display: flex; + flex-direction: column; + gap: 2px; + } + + .device-name-row { + color: var(--text-primary); + font-weight: 500; + } + + .device-id { + color: var(--text-dim); + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + } + + .device-meta { + text-align: right; + color: var(--text-secondary); + font-family: 'JetBrains Mono', monospace; + } + + .device-meta.encrypted { + color: var(--accent-green); + } + + .device-meta.plaintext { + color: var(--accent-red); + } + + .transmission-bar { + width: 60px; + height: 4px; + background: var(--border-color); + position: relative; + } + + .transmission-bar-fill { + height: 100%; + background: var(--accent-cyan); + transition: width 0.3s ease; + } + + .badge { + display: inline-block; + padding: 2px 6px; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1px; + border: 1px solid; + } + + .badge.proto-pocsag { border-color: var(--accent-cyan); color: var(--accent-cyan); } + .badge.proto-flex { border-color: var(--accent-orange); color: var(--accent-orange); } + .badge.proto-433 { border-color: var(--accent-green); color: var(--accent-green); } + .badge.proto-unknown { border-color: var(--text-dim); color: var(--text-dim); } + + .recon-toggle { + padding: 4px 8px; + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-secondary); + cursor: pointer; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1px; + } + + .recon-toggle:hover { + border-color: var(--accent-orange); + color: var(--accent-orange); + } + + .recon-toggle.active { + border-color: var(--accent-orange); + color: var(--accent-orange); + background: rgba(255, 136, 0, 0.1); + } + + .hex-dump { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-dim); + background: var(--bg-primary); + padding: 8px; + margin-top: 8px; + border: 1px solid var(--border-color); + word-break: break-all; + } + + .timeline-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-cyan); + display: inline-block; + margin-right: 5px; + } + + .timeline-dot.recent { background: var(--accent-green); } + .timeline-dot.stale { background: var(--accent-orange); } + .timeline-dot.old { background: var(--text-dim); } + + /* WiFi Visualizations */ + .wifi-visuals { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + padding: 15px; + background: var(--bg-secondary); + margin: 0 15px 10px 15px; + border: 1px solid var(--border-color); + } + + @media (max-width: 1200px) { + .wifi-visuals { grid-template-columns: 1fr; } + } + + .wifi-visual-panel { + background: var(--bg-primary); + border: 1px solid var(--border-color); + padding: 10px; + position: relative; + } + + .wifi-visual-panel h5 { + color: var(--accent-cyan); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid var(--border-color); + } + + /* Radar Display */ + .radar-container { + position: relative; + width: 150px; + height: 150px; + margin: 0 auto; + } + + #radarCanvas, #btRadarCanvas { + width: 100%; + height: 100%; + border-radius: 50%; + background: radial-gradient(circle, #001515 0%, #000a0a 100%); + border: 1px solid var(--accent-cyan-dim); + } + + #btRadarCanvas { + background: radial-gradient(circle, #150015 0%, #0a000a 100%); + border: 1px solid rgba(138, 43, 226, 0.3); + } + + /* Channel Graph */ + .channel-graph { + display: flex; + align-items: flex-end; + justify-content: space-around; + height: 60px; + padding: 5px 0; + border-bottom: 1px solid var(--border-color); + } + + .channel-bar-wrapper { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + } + + .channel-bar { + width: 80%; + background: var(--border-color); + min-height: 2px; + transition: height 0.3s ease, background 0.3s ease; + } + + .channel-bar.active { + background: var(--accent-cyan); + box-shadow: 0 0 5px var(--accent-cyan); + } + + .channel-bar.congested { + background: var(--accent-orange); + } + + .channel-bar.very-congested { + background: var(--accent-red); + } + + .channel-label { + font-size: 8px; + color: var(--text-dim); + margin-top: 3px; + } + + /* Security Donut */ + .security-container { + display: flex; + align-items: center; + gap: 15px; + } + + .security-donut { + width: 80px; + height: 80px; + flex-shrink: 0; + } + + #securityCanvas { + width: 100%; + height: 100%; + } + + .security-legend { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 10px; + font-family: 'JetBrains Mono', monospace; + } + + .security-legend-item { + display: flex; + align-items: center; + gap: 6px; + } + + .security-legend-dot { + width: 10px; + height: 10px; + border-radius: 2px; + } + + .security-legend-dot.wpa3 { background: var(--accent-green); } + .security-legend-dot.wpa2 { background: var(--accent-orange); } + .security-legend-dot.wep { background: var(--accent-red); } + .security-legend-dot.open { background: var(--accent-cyan); } + + /* Signal Strength Meter */ + .signal-strength-display { + text-align: center; + padding: 5px; + } + + .target-ssid { + font-size: 11px; + color: var(--text-secondary); + margin-bottom: 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .signal-value { + font-family: 'JetBrains Mono', monospace; + font-size: 28px; + color: var(--accent-cyan); + text-shadow: 0 0 10px var(--accent-cyan-dim); + } + + .signal-value.weak { color: var(--accent-red); text-shadow: 0 0 10px rgba(255,51,102,0.4); } + .signal-value.medium { color: var(--accent-orange); text-shadow: 0 0 10px rgba(255,136,0,0.4); } + .signal-value.strong { color: var(--accent-green); text-shadow: 0 0 10px rgba(0,255,136,0.4); } + + .signal-bars-large { + display: flex; + justify-content: center; + align-items: flex-end; + gap: 3px; + height: 30px; + margin-top: 8px; + } + + .signal-bar-large { + width: 8px; + background: var(--border-color); + transition: all 0.2s ease; + } + + .signal-bar-large.active { + box-shadow: 0 0 5px currentColor; + } + + .signal-bar-large.weak { background: var(--accent-red); } + .signal-bar-large.medium { background: var(--accent-orange); } + .signal-bar-large.strong { background: var(--accent-green); } + + .signal-bar-large:nth-child(1) { height: 20%; } + .signal-bar-large:nth-child(2) { height: 40%; } + .signal-bar-large:nth-child(3) { height: 60%; } + .signal-bar-large:nth-child(4) { height: 80%; } + .signal-bar-large:nth-child(5) { height: 100%; } + /* Scanline effect overlay */ body::before { content: ''; @@ -782,6 +1248,8 @@ HTML_TEMPLATE = '''
+ +
@@ -924,6 +1392,171 @@ HTML_TEMPLATE = '''
+ +
+
+

WiFi Interface

+
+ +
+ +
+ airmon-ng:Checking... + airodump-ng:Checking... + kismet:Checking... +
+
+ +
+

Monitor Mode

+
+ + +
+
+ Monitor mode: Inactive +
+
+ +
+

Scan Mode

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Attack Options

+
+ ⚠ Only use on authorized networks +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + +
+ + +
+
+

Bluetooth Interface

+
+ +
+ +
+ hcitool:Checking... + bluetoothctl:Checking... + ubertooth:Checking... + bettercap:Checking... +
+
+ +
+

Scan Mode

+
+ + + + +
+
+ + +
+
+ + + +
+
+ +
+

Device Actions

+
+ + +
+
+ + +
+
+ +
+

Attack Options

+
+ ⚠ Authorized testing only +
+
+ + + + +
+
+ + + +
+ @@ -949,6 +1582,128 @@ HTML_TEMPLATE = '''
SENSORS: 0
DEVICES: 0
+ + + + + + + + + + + + +
+
+

ā–¼ Device Intelligence

+
+
TRACKED: 0
+
NEW: 0
+
ANOMALIES: 0
+
+
+
+
+ Device intelligence data will appear here as signals are intercepted. +
@@ -968,10 +1723,12 @@ HTML_TEMPLATE = ''' Idle
+ +
@@ -994,12 +1751,32 @@ HTML_TEMPLATE = ''' function switchMode(mode) { currentMode = mode; document.querySelectorAll('.mode-tab').forEach(tab => { - tab.classList.toggle('active', tab.textContent.toLowerCase().includes(mode === 'pager' ? 'pager' : '433')); + const tabText = tab.textContent.toLowerCase(); + const isActive = (mode === 'pager' && tabText.includes('pager')) || + (mode === 'sensor' && tabText.includes('433')) || + (mode === 'wifi' && tabText.includes('wifi')) || + (mode === 'bluetooth' && tabText === 'bt'); + tab.classList.toggle('active', isActive); }); document.getElementById('pagerMode').classList.toggle('active', mode === 'pager'); document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor'); + document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi'); + document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth'); document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none'; document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none'; + document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none'; + document.getElementById('btStats').style.display = mode === 'bluetooth' ? 'flex' : 'none'; + document.getElementById('wifiVisuals').style.display = mode === 'wifi' ? 'grid' : 'none'; + document.getElementById('btVisuals').style.display = mode === 'bluetooth' ? 'grid' : 'none'; + + // Load interfaces when switching modes + if (mode === 'wifi') { + refreshWifiInterfaces(); + initRadar(); + } else if (mode === 'bluetooth') { + refreshBtInterfaces(); + initBtRadar(); + } } // Track unique sensor devices @@ -1699,6 +2476,1484 @@ HTML_TEMPLATE = ''' document.getElementById('flexCount').textContent = '0'; document.getElementById('sensorCount').textContent = '0'; document.getElementById('deviceCount').textContent = '0'; + + // Reset recon data + deviceDatabase.clear(); + newDeviceAlerts = 0; + anomalyAlerts = 0; + document.getElementById('trackedCount').textContent = '0'; + document.getElementById('newDeviceCount').textContent = '0'; + document.getElementById('anomalyCount').textContent = '0'; + document.getElementById('reconContent').innerHTML = '
Device intelligence data will appear here as signals are intercepted.
'; + } + + // ============== DEVICE INTELLIGENCE & RECONNAISSANCE ============== + + // Device tracking database + const deviceDatabase = new Map(); // key: deviceId, value: device profile + let reconEnabled = localStorage.getItem('reconEnabled') === 'true'; + let newDeviceAlerts = 0; + let anomalyAlerts = 0; + + // Device profile structure + function createDeviceProfile(deviceId, protocol, firstSeen) { + return { + id: deviceId, + protocol: protocol, + firstSeen: firstSeen, + lastSeen: firstSeen, + transmissionCount: 1, + transmissions: [firstSeen], // timestamps of recent transmissions + avgInterval: null, // average time between transmissions + addresses: new Set(), + models: new Set(), + messages: [], + isNew: true, + anomalies: [], + signalStrength: [], + encrypted: null // null = unknown, true/false + }; + } + + // Analyze transmission patterns for anomalies + function analyzeTransmissions(profile) { + const anomalies = []; + const now = Date.now(); + + // Need at least 3 transmissions to analyze patterns + if (profile.transmissions.length < 3) { + return anomalies; + } + + // Calculate intervals between transmissions + const intervals = []; + for (let i = 1; i < profile.transmissions.length; i++) { + intervals.push(profile.transmissions[i] - profile.transmissions[i-1]); + } + + // Calculate average and standard deviation + const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length; + profile.avgInterval = avg; + + const variance = intervals.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / intervals.length; + const stdDev = Math.sqrt(variance); + + // Check for burst transmission (sudden increase in frequency) + const lastInterval = intervals[intervals.length - 1]; + if (avg > 0 && lastInterval < avg * 0.2) { + anomalies.push({ + type: 'burst', + severity: 'medium', + message: 'Burst transmission detected - interval ' + Math.round(lastInterval/1000) + 's vs avg ' + Math.round(avg/1000) + 's' + }); + } + + // Check for silence break (device was quiet, now transmitting again) + if (avg > 0 && lastInterval > avg * 5) { + anomalies.push({ + type: 'silence_break', + severity: 'low', + message: 'Device resumed after ' + Math.round(lastInterval/60000) + ' min silence' + }); + } + + return anomalies; + } + + // Check for encryption indicators + function detectEncryption(message) { + if (!message || message === '[No Message]' || message === '[Tone Only]') { + return null; // Can't determine + } + + // Check for high entropy (random-looking data) + const printableRatio = (message.match(/[a-zA-Z0-9\s.,!?-]/g) || []).length / message.length; + + // Check for common encrypted patterns (hex strings, base64-like) + const hexPattern = /^[0-9A-Fa-f\s]+$/; + const hasNonPrintable = /[^\x20-\x7E]/.test(message); + + if (printableRatio > 0.8 && !hasNonPrintable) { + return false; // Likely plaintext + } else if (hexPattern.test(message.replace(/\s/g, '')) || hasNonPrintable) { + return true; // Likely encrypted or encoded + } + + return null; // Unknown + } + + // Generate device fingerprint + function generateDeviceId(data) { + if (data.protocol && data.protocol.includes('POCSAG')) { + return 'PAGER_' + (data.address || 'UNK'); + } else if (data.protocol === 'FLEX') { + return 'FLEX_' + (data.address || 'UNK'); + } else if (data.model) { + // 433MHz sensor + const id = data.id || data.channel || data.unit || '0'; + return 'SENSOR_' + data.model.replace(/\s+/g, '_') + '_' + id; + } + return 'UNKNOWN_' + Date.now(); + } + + // Track a device transmission + function trackDevice(data) { + const now = Date.now(); + const deviceId = generateDeviceId(data); + const protocol = data.protocol || data.model || 'Unknown'; + + let profile = deviceDatabase.get(deviceId); + let isNewDevice = false; + + if (!profile) { + // New device discovered + profile = createDeviceProfile(deviceId, protocol, now); + isNewDevice = true; + newDeviceAlerts++; + document.getElementById('newDeviceCount').textContent = newDeviceAlerts; + } else { + // Update existing profile + profile.lastSeen = now; + profile.transmissionCount++; + profile.transmissions.push(now); + profile.isNew = false; + + // Keep only last 100 transmissions for analysis + if (profile.transmissions.length > 100) { + profile.transmissions = profile.transmissions.slice(-100); + } + } + + // Track addresses + if (data.address) profile.addresses.add(data.address); + if (data.model) profile.models.add(data.model); + + // Store recent messages (keep last 10) + if (data.message) { + profile.messages.unshift({ + text: data.message, + time: now + }); + if (profile.messages.length > 10) profile.messages.pop(); + + // Detect encryption + const encrypted = detectEncryption(data.message); + if (encrypted !== null) profile.encrypted = encrypted; + } + + // Analyze for anomalies + const newAnomalies = analyzeTransmissions(profile); + if (newAnomalies.length > 0) { + profile.anomalies = profile.anomalies.concat(newAnomalies); + anomalyAlerts += newAnomalies.length; + document.getElementById('anomalyCount').textContent = anomalyAlerts; + } + + deviceDatabase.set(deviceId, profile); + document.getElementById('trackedCount').textContent = deviceDatabase.size; + + // Update recon display + if (reconEnabled) { + updateReconDisplay(deviceId, profile, isNewDevice, newAnomalies); + } + + return { deviceId, profile, isNewDevice, anomalies: newAnomalies }; + } + + // Update reconnaissance display + function updateReconDisplay(deviceId, profile, isNewDevice, anomalies) { + const content = document.getElementById('reconContent'); + + // Remove placeholder if present + const placeholder = content.querySelector('div[style*="text-align: center"]'); + if (placeholder) placeholder.remove(); + + // Check if device row already exists + let row = document.getElementById('device_' + deviceId.replace(/[^a-zA-Z0-9]/g, '_')); + + if (!row) { + // Create new row + row = document.createElement('div'); + row.id = 'device_' + deviceId.replace(/[^a-zA-Z0-9]/g, '_'); + row.className = 'device-row' + (isNewDevice ? ' new-device' : ''); + content.insertBefore(row, content.firstChild); + } + + // Determine protocol badge class + let badgeClass = 'proto-unknown'; + if (profile.protocol.includes('POCSAG')) badgeClass = 'proto-pocsag'; + else if (profile.protocol === 'FLEX') badgeClass = 'proto-flex'; + else if (profile.protocol.includes('SENSOR') || profile.models.size > 0) badgeClass = 'proto-433'; + + // Calculate transmission rate bar width + const maxRate = 100; // Max expected transmissions + const rateWidth = Math.min(100, (profile.transmissionCount / maxRate) * 100); + + // Determine timeline status + const timeSinceLast = Date.now() - profile.lastSeen; + let timelineDot = 'recent'; + if (timeSinceLast > 300000) timelineDot = 'old'; // > 5 min + else if (timeSinceLast > 60000) timelineDot = 'stale'; // > 1 min + + // Build encryption indicator + let encStatus = 'Unknown'; + let encClass = ''; + if (profile.encrypted === true) { encStatus = 'Encrypted'; encClass = 'encrypted'; } + else if (profile.encrypted === false) { encStatus = 'Plaintext'; encClass = 'plaintext'; } + + // Format time + const lastSeenStr = getRelativeTime(new Date(profile.lastSeen).toTimeString().split(' ')[0]); + const firstSeenStr = new Date(profile.firstSeen).toLocaleTimeString(); + + // Update row content + row.className = 'device-row' + (isNewDevice ? ' new-device' : '') + (anomalies.length > 0 ? ' anomaly' : ''); + row.innerHTML = ` +
+
+ + ${profile.protocol.substring(0, 8)} + ${deviceId.substring(0, 30)} +
+
+ First: ${firstSeenStr} | Last: ${lastSeenStr} | TX: ${profile.transmissionCount} + ${profile.avgInterval ? ' | Interval: ' + Math.round(profile.avgInterval/1000) + 's' : ''} +
+
+
${encStatus}
+
+
+
+
+
+
${Array.from(profile.addresses).slice(0, 2).join(', ')}
+ `; + + // Show anomaly alerts + if (anomalies.length > 0) { + anomalies.forEach(a => { + const alertEl = document.createElement('div'); + alertEl.style.cssText = 'padding: 5px 15px; background: rgba(255,51,102,0.1); border-left: 2px solid var(--accent-red); font-size: 10px; color: var(--accent-red);'; + alertEl.textContent = '⚠ ' + a.message; + row.appendChild(alertEl); + }); + } + + // Limit displayed devices + while (content.children.length > 50) { + content.removeChild(content.lastChild); + } + } + + // Toggle recon panel visibility + function toggleRecon() { + reconEnabled = !reconEnabled; + localStorage.setItem('reconEnabled', reconEnabled); + document.getElementById('reconPanel').style.display = reconEnabled ? 'block' : 'none'; + document.getElementById('reconBtn').classList.toggle('active', reconEnabled); + + // Populate recon display if enabled and we have data + if (reconEnabled && deviceDatabase.size > 0) { + deviceDatabase.forEach((profile, deviceId) => { + updateReconDisplay(deviceId, profile, false, []); + }); + } + } + + // Initialize recon state + if (reconEnabled) { + document.getElementById('reconPanel').style.display = 'block'; + document.getElementById('reconBtn').classList.add('active'); + } + + // Hook into existing message handlers to track devices + const originalAddMessage = addMessage; + addMessage = function(msg) { + originalAddMessage(msg); + trackDevice(msg); + }; + + const originalAddSensorReading = addSensorReading; + addSensorReading = function(data) { + originalAddSensorReading(data); + trackDevice(data); + }; + + // Export device database + function exportDeviceDB() { + const data = []; + deviceDatabase.forEach((profile, id) => { + data.push({ + id: id, + protocol: profile.protocol, + firstSeen: new Date(profile.firstSeen).toISOString(), + lastSeen: new Date(profile.lastSeen).toISOString(), + transmissionCount: profile.transmissionCount, + avgIntervalSeconds: profile.avgInterval ? Math.round(profile.avgInterval / 1000) : null, + addresses: Array.from(profile.addresses), + models: Array.from(profile.models), + encrypted: profile.encrypted, + anomalyCount: profile.anomalies.length, + recentMessages: profile.messages.slice(0, 5).map(m => m.text) + }); + }); + downloadFile(JSON.stringify(data, null, 2), 'intercept_device_intelligence.json', 'application/json'); + } + + // Toggle recon panel collapse + function toggleReconCollapse() { + const panel = document.getElementById('reconPanel'); + const icon = document.getElementById('reconCollapseIcon'); + panel.classList.toggle('collapsed'); + icon.textContent = panel.classList.contains('collapsed') ? 'ā–¶' : 'ā–¼'; + } + + // ============== WIFI RECONNAISSANCE ============== + + let wifiEventSource = null; + let isWifiRunning = false; + let monitorInterface = null; + let wifiNetworks = {}; + let wifiClients = {}; + let apCount = 0; + let clientCount = 0; + let handshakeCount = 0; + + // Refresh WiFi interfaces + function refreshWifiInterfaces() { + fetch('/wifi/interfaces') + .then(r => r.json()) + .then(data => { + const select = document.getElementById('wifiInterfaceSelect'); + if (data.interfaces.length === 0) { + select.innerHTML = ''; + } else { + select.innerHTML = data.interfaces.map(i => + `` + ).join(''); + } + + // Update tool status + const statusDiv = document.getElementById('wifiToolStatus'); + statusDiv.innerHTML = ` + airmon-ng:${data.tools.airmon ? 'OK' : 'Missing'} + airodump-ng:${data.tools.airodump ? 'OK' : 'Missing'} + kismet:${data.tools.kismet ? 'OK' : 'Missing'} + `; + + // Update monitor status + if (data.monitor_interface) { + monitorInterface = data.monitor_interface; + updateMonitorStatus(true); + } + }); + } + + // Enable monitor mode + function enableMonitorMode() { + const iface = document.getElementById('wifiInterfaceSelect').value; + if (!iface) { + alert('Please select an interface'); + return; + } + + fetch('/wifi/monitor', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({interface: iface, action: 'start'}) + }).then(r => r.json()) + .then(data => { + if (data.status === 'success') { + monitorInterface = data.monitor_interface; + updateMonitorStatus(true); + showInfo('Monitor mode enabled on ' + monitorInterface); + } else { + alert('Error: ' + data.message); + } + }); + } + + // Disable monitor mode + function disableMonitorMode() { + const iface = monitorInterface || document.getElementById('wifiInterfaceSelect').value; + + fetch('/wifi/monitor', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({interface: iface, action: 'stop'}) + }).then(r => r.json()) + .then(data => { + if (data.status === 'success') { + monitorInterface = null; + updateMonitorStatus(false); + showInfo('Monitor mode disabled'); + } else { + alert('Error: ' + data.message); + } + }); + } + + function updateMonitorStatus(enabled) { + document.getElementById('monitorStartBtn').style.display = enabled ? 'none' : 'block'; + document.getElementById('monitorStopBtn').style.display = enabled ? 'block' : 'none'; + document.getElementById('monitorStatus').innerHTML = enabled + ? 'Monitor mode: Active (' + monitorInterface + ')' + : 'Monitor mode: Inactive'; + } + + // Start WiFi scan + function startWifiScan() { + const scanMode = document.querySelector('input[name="wifiScanMode"]:checked').value; + const band = document.getElementById('wifiBand').value; + const channel = document.getElementById('wifiChannel').value; + + if (!monitorInterface) { + alert('Enable monitor mode first'); + return; + } + + const endpoint = scanMode === 'kismet' ? '/wifi/kismet/start' : '/wifi/scan/start'; + + fetch(endpoint, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + interface: monitorInterface, + band: band, + channel: channel || null + }) + }).then(r => r.json()) + .then(data => { + if (data.status === 'started') { + setWifiRunning(true); + startWifiStream(); + } else { + alert('Error: ' + data.message); + } + }); + } + + // Stop WiFi scan + function stopWifiScan() { + const scanMode = document.querySelector('input[name="wifiScanMode"]:checked').value; + const endpoint = scanMode === 'kismet' ? '/wifi/kismet/stop' : '/wifi/scan/stop'; + + fetch(endpoint, {method: 'POST'}) + .then(r => r.json()) + .then(data => { + setWifiRunning(false); + if (wifiEventSource) { + wifiEventSource.close(); + wifiEventSource = null; + } + }); + } + + function setWifiRunning(running) { + isWifiRunning = running; + document.getElementById('statusDot').classList.toggle('running', running); + document.getElementById('statusText').textContent = running ? 'Scanning...' : 'Idle'; + document.getElementById('startWifiBtn').style.display = running ? 'none' : 'block'; + document.getElementById('stopWifiBtn').style.display = running ? 'block' : 'none'; + } + + // Start WiFi event stream + function startWifiStream() { + if (wifiEventSource) { + wifiEventSource.close(); + } + + wifiEventSource = new EventSource('/wifi/stream'); + + wifiEventSource.onmessage = function(e) { + const data = JSON.parse(e.data); + + if (data.type === 'network') { + handleWifiNetwork(data); + } else if (data.type === 'client') { + handleWifiClient(data); + } else if (data.type === 'info' || data.type === 'raw') { + showInfo(data.text); + } else if (data.type === 'status') { + if (data.text === 'stopped') { + setWifiRunning(false); + } + } + }; + + wifiEventSource.onerror = function() { + console.error('WiFi stream error'); + }; + } + + // Handle discovered WiFi network + function handleWifiNetwork(net) { + const isNew = !wifiNetworks[net.bssid]; + wifiNetworks[net.bssid] = net; + + if (isNew) { + apCount++; + document.getElementById('apCount').textContent = apCount; + playAlert(); + pulseSignal(); + } + + // Update recon display + trackDevice({ + protocol: 'WiFi-AP', + address: net.bssid, + message: net.essid || '[Hidden SSID]', + model: net.essid, + channel: net.channel, + privacy: net.privacy + }); + + // Add to output + addWifiNetworkCard(net, isNew); + } + + // Handle discovered WiFi client + function handleWifiClient(client) { + const isNew = !wifiClients[client.mac]; + wifiClients[client.mac] = client; + + if (isNew) { + clientCount++; + document.getElementById('clientCount').textContent = clientCount; + } + + // Track in device intelligence + trackDevice({ + protocol: 'WiFi-Client', + address: client.mac, + message: client.probes || '[No probes]', + bssid: client.bssid + }); + } + + // Add WiFi network card to output + function addWifiNetworkCard(net, isNew) { + const output = document.getElementById('output'); + const placeholder = output.querySelector('.placeholder'); + if (placeholder) placeholder.remove(); + + // Check if card already exists + let card = document.getElementById('wifi_' + net.bssid.replace(/:/g, '')); + + if (!card) { + card = document.createElement('div'); + card.id = 'wifi_' + net.bssid.replace(/:/g, ''); + card.className = 'sensor-card'; + card.style.borderLeftColor = net.privacy.includes('WPA') ? 'var(--accent-orange)' : + net.privacy.includes('WEP') ? 'var(--accent-red)' : + 'var(--accent-green)'; + output.insertBefore(card, output.firstChild); + } + + const signalStrength = parseInt(net.power) || -100; + const signalBars = Math.max(0, Math.min(5, Math.floor((signalStrength + 100) / 15))); + + card.innerHTML = ` +
+ ${escapeHtml(net.essid || '[Hidden]')} + CH ${net.channel} +
+
+
+
BSSID
+
${net.bssid}
+
+
+
Security
+
${net.privacy}
+
+
+
Signal
+
${net.power} dBm ${'ā–ˆ'.repeat(signalBars)}${'ā–‘'.repeat(5-signalBars)}
+
+
+
Beacons
+
${net.beacons}
+
+
+
+ + +
+ `; + + if (autoScroll) output.scrollTop = 0; + } + + // Target a network for attack + function targetNetwork(bssid, channel) { + document.getElementById('targetBssid').value = bssid; + document.getElementById('wifiChannel').value = channel; + showInfo('Targeted: ' + bssid + ' on channel ' + channel); + } + + // Start handshake capture + function captureHandshake(bssid, channel) { + if (!confirm('Start handshake capture for ' + bssid + '? This will stop the current scan.')) { + return; + } + + fetch('/wifi/handshake/capture', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({bssid: bssid, channel: channel}) + }).then(r => r.json()) + .then(data => { + if (data.status === 'started') { + showInfo('Capturing handshakes for ' + bssid + '. File: ' + data.capture_file); + setWifiRunning(true); + } else { + alert('Error: ' + data.message); + } + }); + } + + // Send deauth + function sendDeauth() { + const bssid = document.getElementById('targetBssid').value; + const client = document.getElementById('targetClient').value || 'FF:FF:FF:FF:FF:FF'; + const count = document.getElementById('deauthCount').value || '5'; + + if (!bssid) { + alert('Enter target BSSID'); + return; + } + + if (!confirm('Send ' + count + ' deauth packets to ' + bssid + '?\\n\\n⚠ Only use on networks you own or have authorization to test!')) { + return; + } + + fetch('/wifi/deauth', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({bssid: bssid, client: client, count: parseInt(count)}) + }).then(r => r.json()) + .then(data => { + if (data.status === 'success') { + showInfo(data.message); + } else { + alert('Error: ' + data.message); + } + }); + } + + // ============== WIFI VISUALIZATIONS ============== + + let radarCtx = null; + let radarAngle = 0; + let radarAnimFrame = null; + let radarNetworks = []; // {x, y, strength, ssid, bssid} + let targetBssidForSignal = null; + + // Initialize radar canvas + function initRadar() { + const canvas = document.getElementById('radarCanvas'); + if (!canvas) return; + + radarCtx = canvas.getContext('2d'); + canvas.width = 150; + canvas.height = 150; + + // Start animation + if (!radarAnimFrame) { + animateRadar(); + } + } + + // Animate radar sweep + function animateRadar() { + if (!radarCtx) { + radarAnimFrame = null; + return; + } + + const canvas = radarCtx.canvas; + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const radius = Math.min(cx, cy) - 5; + + // Clear canvas + radarCtx.fillStyle = 'rgba(0, 10, 10, 0.1)'; + radarCtx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw grid circles + radarCtx.strokeStyle = 'rgba(0, 212, 255, 0.2)'; + radarCtx.lineWidth = 1; + for (let r = radius / 4; r <= radius; r += radius / 4) { + radarCtx.beginPath(); + radarCtx.arc(cx, cy, r, 0, Math.PI * 2); + radarCtx.stroke(); + } + + // Draw crosshairs + radarCtx.beginPath(); + radarCtx.moveTo(cx, cy - radius); + radarCtx.lineTo(cx, cy + radius); + radarCtx.moveTo(cx - radius, cy); + radarCtx.lineTo(cx + radius, cy); + radarCtx.stroke(); + + // Draw sweep line + radarCtx.strokeStyle = 'rgba(0, 255, 136, 0.8)'; + radarCtx.lineWidth = 2; + radarCtx.beginPath(); + radarCtx.moveTo(cx, cy); + radarCtx.lineTo( + cx + Math.cos(radarAngle) * radius, + cy + Math.sin(radarAngle) * radius + ); + radarCtx.stroke(); + + // Draw sweep gradient + const gradient = radarCtx.createConicalGradient ? + null : // Not supported in all browsers + radarCtx.createRadialGradient(cx, cy, 0, cx, cy, radius); + + radarCtx.fillStyle = 'rgba(0, 255, 136, 0.05)'; + radarCtx.beginPath(); + radarCtx.moveTo(cx, cy); + radarCtx.arc(cx, cy, radius, radarAngle - 0.5, radarAngle); + radarCtx.closePath(); + radarCtx.fill(); + + // Draw network blips + radarNetworks.forEach(net => { + const age = Date.now() - net.timestamp; + const alpha = Math.max(0.1, 1 - age / 10000); + + radarCtx.fillStyle = `rgba(0, 255, 136, ${alpha})`; + radarCtx.beginPath(); + radarCtx.arc(net.x, net.y, 4 + (1 - alpha) * 3, 0, Math.PI * 2); + radarCtx.fill(); + + // Glow effect + radarCtx.fillStyle = `rgba(0, 255, 136, ${alpha * 0.3})`; + radarCtx.beginPath(); + radarCtx.arc(net.x, net.y, 8 + (1 - alpha) * 5, 0, Math.PI * 2); + radarCtx.fill(); + }); + + // Update angle + radarAngle += 0.03; + if (radarAngle > Math.PI * 2) radarAngle = 0; + + radarAnimFrame = requestAnimationFrame(animateRadar); + } + + // Add network to radar + function addNetworkToRadar(net) { + const canvas = document.getElementById('radarCanvas'); + if (!canvas) return; + + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const radius = Math.min(cx, cy) - 10; + + // Convert signal strength to distance (stronger = closer) + const power = parseInt(net.power) || -80; + const distance = Math.max(0.1, Math.min(1, (power + 100) / 60)); + const r = radius * (1 - distance); + + // Random angle based on BSSID hash + let angle = 0; + for (let i = 0; i < net.bssid.length; i++) { + angle += net.bssid.charCodeAt(i); + } + angle = (angle % 360) * Math.PI / 180; + + const x = cx + Math.cos(angle) * r; + const y = cy + Math.sin(angle) * r; + + // Update or add + const existing = radarNetworks.find(n => n.bssid === net.bssid); + if (existing) { + existing.x = x; + existing.y = y; + existing.timestamp = Date.now(); + } else { + radarNetworks.push({ + x, y, + bssid: net.bssid, + ssid: net.essid, + timestamp: Date.now() + }); + } + + // Limit to 50 networks + if (radarNetworks.length > 50) { + radarNetworks.shift(); + } + } + + // Update channel graph + function updateChannelGraph() { + const channels = {}; + for (let i = 1; i <= 13; i++) channels[i] = 0; + + // Count networks per channel + Object.values(wifiNetworks).forEach(net => { + const ch = parseInt(net.channel); + if (ch >= 1 && ch <= 13) { + channels[ch]++; + } + }); + + // Find max for scaling + const maxCount = Math.max(1, ...Object.values(channels)); + + // Update bars + const bars = document.querySelectorAll('#channelGraph .channel-bar'); + bars.forEach((bar, i) => { + const ch = i + 1; + const count = channels[ch] || 0; + const height = Math.max(2, (count / maxCount) * 55); + bar.style.height = height + 'px'; + + bar.classList.remove('active', 'congested', 'very-congested'); + if (count > 0) bar.classList.add('active'); + if (count >= 3) bar.classList.add('congested'); + if (count >= 5) bar.classList.add('very-congested'); + }); + } + + // Update security donut chart + function updateSecurityDonut() { + const canvas = document.getElementById('securityCanvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const radius = Math.min(cx, cy) - 2; + const innerRadius = radius * 0.6; + + // Count security types + let wpa3 = 0, wpa2 = 0, wep = 0, open = 0; + Object.values(wifiNetworks).forEach(net => { + const priv = (net.privacy || '').toUpperCase(); + if (priv.includes('WPA3')) wpa3++; + else if (priv.includes('WPA')) wpa2++; + else if (priv.includes('WEP')) wep++; + else if (priv === 'OPN' || priv === '' || priv === 'OPEN') open++; + else wpa2++; // Default to WPA2 + }); + + const total = wpa3 + wpa2 + wep + open; + + // Update legend + document.getElementById('wpa3Count').textContent = wpa3; + document.getElementById('wpa2Count').textContent = wpa2; + document.getElementById('wepCount').textContent = wep; + document.getElementById('openCount').textContent = open; + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (total === 0) { + // Draw empty circle + ctx.strokeStyle = '#1a1a1a'; + ctx.lineWidth = radius - innerRadius; + ctx.beginPath(); + ctx.arc(cx, cy, (radius + innerRadius) / 2, 0, Math.PI * 2); + ctx.stroke(); + return; + } + + // Draw segments + const colors = { + wpa3: '#00ff88', + wpa2: '#ff8800', + wep: '#ff3366', + open: '#00d4ff' + }; + + const data = [ + { value: wpa3, color: colors.wpa3 }, + { value: wpa2, color: colors.wpa2 }, + { value: wep, color: colors.wep }, + { value: open, color: colors.open } + ]; + + let startAngle = -Math.PI / 2; + + data.forEach(segment => { + if (segment.value === 0) return; + + const sliceAngle = (segment.value / total) * Math.PI * 2; + + ctx.fillStyle = segment.color; + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, radius, startAngle, startAngle + sliceAngle); + ctx.closePath(); + ctx.fill(); + + startAngle += sliceAngle; + }); + + // Draw inner circle (donut hole) + ctx.fillStyle = '#000'; + ctx.beginPath(); + ctx.arc(cx, cy, innerRadius, 0, Math.PI * 2); + ctx.fill(); + + // Draw total in center + ctx.fillStyle = '#fff'; + ctx.font = 'bold 16px JetBrains Mono'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(total, cx, cy); + } + + // Update signal strength meter for targeted network + function updateSignalMeter(net) { + if (!net) return; + + targetBssidForSignal = net.bssid; + + const ssidEl = document.getElementById('targetSsid'); + const valueEl = document.getElementById('signalValue'); + const barsEl = document.querySelectorAll('.signal-bar-large'); + + ssidEl.textContent = net.essid || net.bssid; + + const power = parseInt(net.power) || -100; + valueEl.textContent = power + ' dBm'; + + // Determine signal quality + let quality = 'weak'; + let activeBars = 1; + + if (power >= -50) { quality = 'strong'; activeBars = 5; } + else if (power >= -60) { quality = 'strong'; activeBars = 4; } + else if (power >= -70) { quality = 'medium'; activeBars = 3; } + else if (power >= -80) { quality = 'medium'; activeBars = 2; } + else { quality = 'weak'; activeBars = 1; } + + valueEl.className = 'signal-value ' + quality; + + barsEl.forEach((bar, i) => { + bar.className = 'signal-bar-large'; + if (i < activeBars) { + bar.classList.add('active', quality); + } + }); + } + + // Hook into handleWifiNetwork to update visualizations + const originalHandleWifiNetwork = handleWifiNetwork; + handleWifiNetwork = function(net) { + originalHandleWifiNetwork(net); + + // Update radar + addNetworkToRadar(net); + + // Update channel graph + updateChannelGraph(); + + // Update security donut + updateSecurityDonut(); + + // Update signal meter if this is the targeted network + if (targetBssidForSignal === net.bssid) { + updateSignalMeter(net); + } + }; + + // Update targetNetwork to also set signal meter + const originalTargetNetwork = targetNetwork; + targetNetwork = function(bssid, channel) { + originalTargetNetwork(bssid, channel); + + const net = wifiNetworks[bssid]; + if (net) { + updateSignalMeter(net); + } + }; + + // ============== BLUETOOTH RECONNAISSANCE ============== + + let btEventSource = null; + let isBtRunning = false; + let btDevices = {}; + let btDeviceCount = 0; + let btBeaconCount = 0; + let btTrackerCount = 0; + let btRadarCtx = null; + let btRadarAngle = 0; + let btRadarAnimFrame = null; + let btRadarDevices = []; + + // Refresh Bluetooth interfaces + function refreshBtInterfaces() { + fetch('/bt/interfaces') + .then(r => r.json()) + .then(data => { + const select = document.getElementById('btInterfaceSelect'); + if (data.interfaces.length === 0) { + select.innerHTML = ''; + } else { + select.innerHTML = data.interfaces.map(i => + `` + ).join(''); + } + + // Update tool status + const statusDiv = document.getElementById('btToolStatus'); + statusDiv.innerHTML = ` + hcitool:${data.tools.hcitool ? 'OK' : 'Missing'} + bluetoothctl:${data.tools.bluetoothctl ? 'OK' : 'Missing'} + ubertooth:${data.tools.ubertooth ? 'OK' : 'Missing'} + bettercap:${data.tools.bettercap ? 'OK' : 'Missing'} + `; + }); + } + + // Start Bluetooth scan + function startBtScan() { + const scanMode = document.querySelector('input[name="btScanMode"]:checked').value; + const iface = document.getElementById('btInterfaceSelect').value; + const duration = document.getElementById('btScanDuration').value; + const scanBLE = document.getElementById('btScanBLE').checked; + const scanClassic = document.getElementById('btScanClassic').checked; + + fetch('/bt/scan/start', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + mode: scanMode, + interface: iface, + duration: parseInt(duration), + scan_ble: scanBLE, + scan_classic: scanClassic + }) + }).then(r => r.json()) + .then(data => { + if (data.status === 'started') { + setBtRunning(true); + startBtStream(); + } else { + alert('Error: ' + data.message); + } + }); + } + + // Stop Bluetooth scan + function stopBtScan() { + fetch('/bt/scan/stop', {method: 'POST'}) + .then(r => r.json()) + .then(data => { + setBtRunning(false); + if (btEventSource) { + btEventSource.close(); + btEventSource = null; + } + }); + } + + function setBtRunning(running) { + isBtRunning = running; + document.getElementById('statusDot').classList.toggle('running', running); + document.getElementById('statusText').textContent = running ? 'Scanning...' : 'Idle'; + document.getElementById('startBtBtn').style.display = running ? 'none' : 'block'; + document.getElementById('stopBtBtn').style.display = running ? 'block' : 'none'; + } + + // Start Bluetooth event stream + function startBtStream() { + if (btEventSource) btEventSource.close(); + + btEventSource = new EventSource('/bt/stream'); + + btEventSource.onmessage = function(e) { + const data = JSON.parse(e.data); + + if (data.type === 'device') { + handleBtDevice(data); + } else if (data.type === 'info' || data.type === 'raw') { + showInfo(data.text); + } else if (data.type === 'status') { + if (data.text === 'stopped') { + setBtRunning(false); + } + } + }; + + btEventSource.onerror = function() { + console.error('BT stream error'); + }; + } + + // Handle discovered Bluetooth device + function handleBtDevice(device) { + const isNew = !btDevices[device.mac]; + btDevices[device.mac] = device; + + if (isNew) { + btDeviceCount++; + document.getElementById('btDeviceCount').textContent = btDeviceCount; + playAlert(); + pulseSignal(); + + if (device.tracker) { + btTrackerCount++; + document.getElementById('btTrackerCount').textContent = btTrackerCount; + addTrackerAlert(device); + } + } + + // Track in device intelligence + trackDevice({ + protocol: 'Bluetooth', + address: device.mac, + message: device.name, + model: device.manufacturer, + type: device.type + }); + + // Update visualizations + addBtDeviceToRadar(device); + updateBtTypeChart(); + updateBtManufacturerList(); + + // Add device card + addBtDeviceCard(device, isNew); + } + + // Add Bluetooth device card to output + function addBtDeviceCard(device, isNew) { + const output = document.getElementById('output'); + const placeholder = output.querySelector('.placeholder'); + if (placeholder) placeholder.remove(); + + let card = document.getElementById('bt_' + device.mac.replace(/:/g, '')); + + if (!card) { + card = document.createElement('div'); + card.id = 'bt_' + device.mac.replace(/:/g, ''); + card.className = 'sensor-card'; + card.style.borderLeftColor = device.tracker ? 'var(--accent-red)' : + device.type === 'phone' ? 'var(--accent-cyan)' : + device.type === 'audio' ? 'var(--accent-green)' : + 'var(--accent-orange)'; + output.insertBefore(card, output.firstChild); + } + + const typeIcon = { + 'phone': 'šŸ“±', 'audio': 'šŸŽ§', 'wearable': '⌚', 'tracker': 'šŸ“', + 'computer': 'šŸ’»', 'input': 'āŒØļø', 'other': 'šŸ“¶' + }[device.type] || 'šŸ“¶'; + + card.innerHTML = ` +
+ ${typeIcon} ${escapeHtml(device.name)} + ${device.type.toUpperCase()} +
+
+
+
MAC
+
${device.mac}
+
+
+
Manufacturer
+
${device.manufacturer}
+
+ ${device.tracker ? ` +
+
Tracker
+
${device.tracker.name}
+
` : ''} +
+
+ + +
+ `; + + if (autoScroll) output.scrollTop = 0; + } + + // Add tracker alert to visualization + function addTrackerAlert(device) { + const list = document.getElementById('btTrackerList'); + const placeholder = list.querySelector('div[style*="text-align: center"]'); + if (placeholder) placeholder.remove(); + + const alert = document.createElement('div'); + alert.style.cssText = 'padding: 8px; margin-bottom: 5px; background: rgba(255,51,102,0.1); border-left: 2px solid var(--accent-red); font-family: JetBrains Mono, monospace;'; + alert.innerHTML = ` +
⚠ ${device.tracker.name} Detected
+
${device.mac}
+ `; + list.insertBefore(alert, list.firstChild); + } + + // Target a Bluetooth device + function btTargetDevice(mac) { + document.getElementById('btTargetMac').value = mac; + showInfo('Targeted: ' + mac); + } + + // Enumerate services for a device + function btEnumServicesFor(mac) { + document.getElementById('btTargetMac').value = mac; + btEnumServices(); + } + + // Enumerate services + function btEnumServices() { + const mac = document.getElementById('btTargetMac').value; + if (!mac) { alert('Enter target MAC'); return; } + + showInfo('Enumerating services for ' + mac + '...'); + + fetch('/bt/enum', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({mac: mac}) + }).then(r => r.json()) + .then(data => { + if (data.status === 'success') { + let msg = 'Services for ' + mac + ': '; + if (data.services.length === 0) { + msg += 'None found'; + } else { + msg += data.services.map(s => s.name).join(', '); + } + showInfo(msg); + } else { + showInfo('Error: ' + data.message); + } + }); + } + + // L2CAP Ping + function btPing() { + const mac = document.getElementById('btTargetMac').value; + if (!mac) { alert('Enter target MAC'); return; } + + fetch('/bt/ping', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({mac: mac, count: 5}) + }).then(r => r.json()) + .then(data => { + if (data.status === 'success') { + showInfo('Ping ' + mac + ': ' + (data.reachable ? 'Reachable' : 'Unreachable')); + } else { + showInfo('Ping error: ' + data.message); + } + }); + } + + // DoS attack + function btDosAttack() { + const mac = document.getElementById('btTargetMac').value; + if (!mac) { alert('Enter target MAC'); return; } + + if (!confirm('Send DoS ping flood to ' + mac + '?\\n\\n⚠ Only test on devices you own!')) return; + + showInfo('Starting DoS test on ' + mac + '...'); + + fetch('/bt/dos', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({mac: mac, count: 100, size: 600}) + }).then(r => r.json()) + .then(data => { + showInfo('DoS test complete: ' + (data.message || 'Done')); + }); + } + + // Stub functions for other attacks + function btReplayAttack() { alert('Replay attack requires captured packets'); } + function btSpoofMac() { alert('MAC spoofing requires root privileges'); } + function btScanVulns() { alert('Vulnerability scanning not yet implemented'); } + + // Initialize Bluetooth radar + function initBtRadar() { + const canvas = document.getElementById('btRadarCanvas'); + if (!canvas) return; + + btRadarCtx = canvas.getContext('2d'); + canvas.width = 150; + canvas.height = 150; + + if (!btRadarAnimFrame) { + animateBtRadar(); + } + } + + // Animate Bluetooth radar + function animateBtRadar() { + if (!btRadarCtx) { btRadarAnimFrame = null; return; } + + const canvas = btRadarCtx.canvas; + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const radius = Math.min(cx, cy) - 5; + + btRadarCtx.fillStyle = 'rgba(0, 10, 20, 0.1)'; + btRadarCtx.fillRect(0, 0, canvas.width, canvas.height); + + // Grid circles + btRadarCtx.strokeStyle = 'rgba(138, 43, 226, 0.2)'; + btRadarCtx.lineWidth = 1; + for (let r = radius / 4; r <= radius; r += radius / 4) { + btRadarCtx.beginPath(); + btRadarCtx.arc(cx, cy, r, 0, Math.PI * 2); + btRadarCtx.stroke(); + } + + // Sweep line (purple for BT) + btRadarCtx.strokeStyle = 'rgba(138, 43, 226, 0.8)'; + btRadarCtx.lineWidth = 2; + btRadarCtx.beginPath(); + btRadarCtx.moveTo(cx, cy); + btRadarCtx.lineTo(cx + Math.cos(btRadarAngle) * radius, cy + Math.sin(btRadarAngle) * radius); + btRadarCtx.stroke(); + + // Device blips + btRadarDevices.forEach(dev => { + const age = Date.now() - dev.timestamp; + const alpha = Math.max(0.1, 1 - age / 15000); + const color = dev.isTracker ? '255, 51, 102' : '138, 43, 226'; + + btRadarCtx.fillStyle = `rgba(${color}, ${alpha})`; + btRadarCtx.beginPath(); + btRadarCtx.arc(dev.x, dev.y, dev.isTracker ? 6 : 4, 0, Math.PI * 2); + btRadarCtx.fill(); + }); + + btRadarAngle += 0.025; + if (btRadarAngle > Math.PI * 2) btRadarAngle = 0; + + btRadarAnimFrame = requestAnimationFrame(animateBtRadar); + } + + // Add device to BT radar + function addBtDeviceToRadar(device) { + const canvas = document.getElementById('btRadarCanvas'); + if (!canvas) return; + + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const radius = Math.min(cx, cy) - 10; + + // Random position based on MAC hash + let angle = 0; + for (let i = 0; i < device.mac.length; i++) { + angle += device.mac.charCodeAt(i); + } + angle = (angle % 360) * Math.PI / 180; + const r = radius * (0.3 + Math.random() * 0.6); + + const x = cx + Math.cos(angle) * r; + const y = cy + Math.sin(angle) * r; + + const existing = btRadarDevices.find(d => d.mac === device.mac); + if (existing) { + existing.timestamp = Date.now(); + } else { + btRadarDevices.push({ + x, y, + mac: device.mac, + isTracker: !!device.tracker, + timestamp: Date.now() + }); + } + + if (btRadarDevices.length > 50) btRadarDevices.shift(); + } + + // Update device type chart + function updateBtTypeChart() { + const canvas = document.getElementById('btTypeCanvas'); + if (!canvas) return; + + let phones = 0, audio = 0, wearables = 0, trackers = 0, other = 0; + + Object.values(btDevices).forEach(d => { + if (d.tracker) trackers++; + else if (d.type === 'phone') phones++; + else if (d.type === 'audio') audio++; + else if (d.type === 'wearable') wearables++; + else other++; + }); + + document.getElementById('btPhoneCount').textContent = phones; + document.getElementById('btAudioCount').textContent = audio; + document.getElementById('btWearableCount').textContent = wearables; + document.getElementById('btTrackerTypeCount').textContent = trackers; + document.getElementById('btOtherCount').textContent = other; + + // Draw donut + const ctx = canvas.getContext('2d'); + const cx = canvas.width / 2; + const cy = canvas.height / 2; + const r = Math.min(cx, cy) - 2; + const inner = r * 0.6; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const total = phones + audio + wearables + trackers + other; + if (total === 0) return; + + const data = [ + { value: phones, color: '#00d4ff' }, + { value: audio, color: '#00ff88' }, + { value: wearables, color: '#ff8800' }, + { value: trackers, color: '#ff3366' }, + { value: other, color: '#888' } + ]; + + let start = -Math.PI / 2; + data.forEach(seg => { + if (seg.value === 0) return; + const angle = (seg.value / total) * Math.PI * 2; + ctx.fillStyle = seg.color; + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.arc(cx, cy, r, start, start + angle); + ctx.closePath(); + ctx.fill(); + start += angle; + }); + + ctx.fillStyle = '#000'; + ctx.beginPath(); + ctx.arc(cx, cy, inner, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = '#fff'; + ctx.font = 'bold 14px JetBrains Mono'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(total, cx, cy); + } + + // Update manufacturer list + function updateBtManufacturerList() { + const manufacturers = {}; + Object.values(btDevices).forEach(d => { + const m = d.manufacturer || 'Unknown'; + manufacturers[m] = (manufacturers[m] || 0) + 1; + }); + + const sorted = Object.entries(manufacturers).sort((a, b) => b[1] - a[1]).slice(0, 6); + + const list = document.getElementById('btManufacturerList'); + if (sorted.length === 0) { + list.innerHTML = '
Scanning for devices...
'; + } else { + list.innerHTML = sorted.map(([name, count]) => + `
${name}${count}
` + ).join(''); + } } @@ -2092,30 +4347,22 @@ def log_message(msg): @app.route('/killall', methods=['POST']) def kill_all(): - """Kill all rtl_fm, multimon-ng, and rtl_433 processes.""" - global current_process, sensor_process + """Kill all decoder and WiFi processes.""" + global current_process, sensor_process, wifi_process, kismet_process killed = [] - try: - result = subprocess.run(['pkill', '-f', 'rtl_fm'], capture_output=True) - if result.returncode == 0: - killed.append('rtl_fm') - except: - pass + processes_to_kill = [ + 'rtl_fm', 'multimon-ng', 'rtl_433', + 'airodump-ng', 'aireplay-ng', 'airmon-ng', 'kismet' + ] - try: - result = subprocess.run(['pkill', '-f', 'multimon-ng'], capture_output=True) - if result.returncode == 0: - killed.append('multimon-ng') - except: - pass - - try: - result = subprocess.run(['pkill', '-f', 'rtl_433'], capture_output=True) - if result.returncode == 0: - killed.append('rtl_433') - except: - pass + for proc in processes_to_kill: + try: + result = subprocess.run(['pkill', '-f', proc], capture_output=True) + if result.returncode == 0: + killed.append(proc) + except: + pass with process_lock: current_process = None @@ -2123,6 +4370,10 @@ def kill_all(): with sensor_lock: sensor_process = None + with wifi_lock: + wifi_process = None + kismet_process = None + return jsonify({'status': 'killed', 'processes': killed}) @@ -2296,10 +4547,1069 @@ def stream_sensor(): return response +# ============== WIFI RECONNAISSANCE ROUTES ============== + +def detect_wifi_interfaces(): + """Detect available WiFi interfaces.""" + interfaces = [] + import platform + + if platform.system() == 'Darwin': # macOS + try: + # Get list of network interfaces + result = subprocess.run(['networksetup', '-listallhardwareports'], + capture_output=True, text=True, timeout=5) + lines = result.stdout.split('\n') + current_device = None + for i, line in enumerate(lines): + if 'Wi-Fi' in line or 'AirPort' in line: + # Next line should have the device + for j in range(i+1, min(i+3, len(lines))): + if 'Device:' in lines[j]: + device = lines[j].split('Device:')[1].strip() + interfaces.append({ + 'name': device, + 'type': 'internal', + 'monitor_capable': False, # macOS internal usually can't + 'status': 'up' + }) + break + except Exception as e: + print(f"[WiFi] Error detecting macOS interfaces: {e}") + + # Check for USB WiFi adapters + try: + result = subprocess.run(['system_profiler', 'SPUSBDataType'], + capture_output=True, text=True, timeout=10) + if 'Wireless' in result.stdout or 'WLAN' in result.stdout or '802.11' in result.stdout: + interfaces.append({ + 'name': 'USB WiFi Adapter', + 'type': 'usb', + 'monitor_capable': True, + 'status': 'detected' + }) + except Exception: + pass + + else: # Linux + try: + # Use iw to list wireless interfaces + result = subprocess.run(['iw', 'dev'], capture_output=True, text=True, timeout=5) + current_iface = None + for line in result.stdout.split('\n'): + line = line.strip() + if line.startswith('Interface'): + current_iface = line.split()[1] + elif current_iface and 'type' in line: + iface_type = line.split()[-1] + interfaces.append({ + 'name': current_iface, + 'type': iface_type, + 'monitor_capable': True, + 'status': 'up' + }) + current_iface = None + except FileNotFoundError: + # Try iwconfig instead + try: + result = subprocess.run(['iwconfig'], capture_output=True, text=True, timeout=5) + for line in result.stdout.split('\n'): + if 'IEEE 802.11' in line: + iface = line.split()[0] + interfaces.append({ + 'name': iface, + 'type': 'managed', + 'monitor_capable': True, + 'status': 'up' + }) + except Exception: + pass + except Exception as e: + print(f"[WiFi] Error detecting Linux interfaces: {e}") + + return interfaces + + +@app.route('/wifi/interfaces') +def get_wifi_interfaces(): + """Get available WiFi interfaces.""" + interfaces = detect_wifi_interfaces() + tools = { + 'airmon': check_tool('airmon-ng'), + 'airodump': check_tool('airodump-ng'), + 'aireplay': check_tool('aireplay-ng'), + 'kismet': check_tool('kismet'), + 'iw': check_tool('iw') + } + return jsonify({'interfaces': interfaces, 'tools': tools, 'monitor_interface': wifi_monitor_interface}) + + +@app.route('/wifi/monitor', methods=['POST']) +def toggle_monitor_mode(): + """Enable or disable monitor mode on an interface.""" + global wifi_monitor_interface + + data = request.json + interface = data.get('interface') + action = data.get('action', 'start') # 'start' or 'stop' + + if not interface: + return jsonify({'status': 'error', 'message': 'No interface specified'}) + + if action == 'start': + # Try airmon-ng first + if check_tool('airmon-ng'): + try: + # Kill interfering processes + subprocess.run(['airmon-ng', 'check', 'kill'], capture_output=True, timeout=10) + + # Start monitor mode + result = subprocess.run(['airmon-ng', 'start', interface], + capture_output=True, text=True, timeout=15) + + # Parse output to find monitor interface name + output = result.stdout + result.stderr + # Common patterns: wlan0mon, wlan0, mon0 + import re + match = re.search(r'monitor mode.*?enabled.*?(\w+mon|\w+)', output, re.IGNORECASE) + if match: + wifi_monitor_interface = match.group(1) + else: + # Assume it's interface + 'mon' + wifi_monitor_interface = interface + 'mon' + + wifi_queue.put({'type': 'info', 'text': f'Monitor mode enabled on {wifi_monitor_interface}'}) + return jsonify({'status': 'success', 'monitor_interface': wifi_monitor_interface}) + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + # Fallback to iw (Linux) + elif check_tool('iw'): + try: + subprocess.run(['ip', 'link', 'set', interface, 'down'], capture_output=True) + subprocess.run(['iw', interface, 'set', 'monitor', 'control'], capture_output=True) + subprocess.run(['ip', 'link', 'set', interface, 'up'], capture_output=True) + wifi_monitor_interface = interface + return jsonify({'status': 'success', 'monitor_interface': interface}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + else: + return jsonify({'status': 'error', 'message': 'No monitor mode tools available (need airmon-ng or iw)'}) + + else: # stop + if check_tool('airmon-ng'): + try: + result = subprocess.run(['airmon-ng', 'stop', wifi_monitor_interface or interface], + capture_output=True, text=True, timeout=15) + wifi_monitor_interface = None + return jsonify({'status': 'success', 'message': 'Monitor mode disabled'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + elif check_tool('iw'): + try: + subprocess.run(['ip', 'link', 'set', interface, 'down'], capture_output=True) + subprocess.run(['iw', interface, 'set', 'type', 'managed'], capture_output=True) + subprocess.run(['ip', 'link', 'set', interface, 'up'], capture_output=True) + wifi_monitor_interface = None + return jsonify({'status': 'success', 'message': 'Monitor mode disabled'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + return jsonify({'status': 'error', 'message': 'Unknown action'}) + + +def parse_airodump_csv(csv_path): + """Parse airodump-ng CSV output file.""" + networks = {} + clients = {} + + try: + with open(csv_path, 'r', errors='replace') as f: + content = f.read() + + # Split into networks and clients sections + sections = content.split('\n\n') + + for section in sections: + lines = section.strip().split('\n') + if not lines: + continue + + header = lines[0] if lines else '' + + if 'BSSID' in header and 'ESSID' in header: + # Networks section + for line in lines[1:]: + parts = [p.strip() for p in line.split(',')] + if len(parts) >= 14: + bssid = parts[0] + if bssid and ':' in bssid: + networks[bssid] = { + 'bssid': bssid, + 'first_seen': parts[1], + 'last_seen': parts[2], + 'channel': parts[3], + 'speed': parts[4], + 'privacy': parts[5], + 'cipher': parts[6], + 'auth': parts[7], + 'power': parts[8], + 'beacons': parts[9], + 'ivs': parts[10], + 'lan_ip': parts[11], + 'essid': parts[13] if len(parts) > 13 else 'Hidden' + } + + elif 'Station MAC' in header: + # Clients section + for line in lines[1:]: + parts = [p.strip() for p in line.split(',')] + if len(parts) >= 6: + station = parts[0] + if station and ':' in station: + clients[station] = { + 'mac': station, + 'first_seen': parts[1], + 'last_seen': parts[2], + 'power': parts[3], + 'packets': parts[4], + 'bssid': parts[5], + 'probes': parts[6] if len(parts) > 6 else '' + } + except Exception as e: + print(f"[WiFi] Error parsing CSV: {e}") + + return networks, clients + + +def stream_airodump_output(process, csv_path): + """Stream airodump-ng output to queue.""" + global wifi_process, wifi_networks, wifi_clients + import time + + try: + wifi_queue.put({'type': 'status', 'text': 'started'}) + last_parse = 0 + + while process.poll() is None: + # Parse CSV file periodically + current_time = time.time() + if current_time - last_parse >= 2: # Parse every 2 seconds + if os.path.exists(csv_path + '-01.csv'): + networks, clients = parse_airodump_csv(csv_path + '-01.csv') + + # Detect new networks + for bssid, net in networks.items(): + if bssid not in wifi_networks: + wifi_queue.put({ + 'type': 'network', + 'action': 'new', + **net + }) + else: + # Update existing + wifi_queue.put({ + 'type': 'network', + 'action': 'update', + **net + }) + + # Detect new clients + for mac, client in clients.items(): + if mac not in wifi_clients: + wifi_queue.put({ + 'type': 'client', + 'action': 'new', + **client + }) + + wifi_networks = networks + wifi_clients = clients + last_parse = current_time + + time.sleep(0.5) + + except Exception as e: + wifi_queue.put({'type': 'error', 'text': str(e)}) + finally: + process.wait() + wifi_queue.put({'type': 'status', 'text': 'stopped'}) + with wifi_lock: + wifi_process = None + + +@app.route('/wifi/scan/start', methods=['POST']) +def start_wifi_scan(): + """Start WiFi scanning with airodump-ng.""" + global wifi_process, wifi_networks, wifi_clients + + with wifi_lock: + if wifi_process: + return jsonify({'status': 'error', 'message': 'Scan already running'}) + + data = request.json + interface = data.get('interface') or wifi_monitor_interface + channel = data.get('channel') # None = channel hopping + band = data.get('band', 'abg') # 'a' = 5GHz, 'bg' = 2.4GHz, 'abg' = both + + if not interface: + return jsonify({'status': 'error', 'message': 'No monitor interface available. Enable monitor mode first.'}) + + # Clear previous data + wifi_networks = {} + wifi_clients = {} + + # Clear queue + while not wifi_queue.empty(): + try: + wifi_queue.get_nowait() + except: + break + + # Build airodump-ng command + csv_path = '/tmp/intercept_wifi' + + # Remove old files + for f in [f'/tmp/intercept_wifi-01.csv', f'/tmp/intercept_wifi-01.cap', + f'/tmp/intercept_wifi-01.kismet.csv', f'/tmp/intercept_wifi-01.kismet.netxml']: + try: + os.remove(f) + except: + pass + + cmd = [ + 'airodump-ng', + '-w', csv_path, + '--output-format', 'csv,pcap', + '--band', band, + interface + ] + + if channel: + cmd.extend(['-c', str(channel)]) + + print(f"[WiFi] Running: {' '.join(cmd)}") + + try: + wifi_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # Start parsing thread + thread = threading.Thread(target=stream_airodump_output, args=(wifi_process, csv_path)) + thread.daemon = True + thread.start() + + wifi_queue.put({'type': 'info', 'text': f'Started scanning on {interface}'}) + + return jsonify({'status': 'started', 'interface': interface}) + + except FileNotFoundError: + return jsonify({'status': 'error', 'message': 'airodump-ng not found. Install aircrack-ng suite.'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/wifi/scan/stop', methods=['POST']) +def stop_wifi_scan(): + """Stop WiFi scanning.""" + global wifi_process + + with wifi_lock: + if wifi_process: + wifi_process.terminate() + try: + wifi_process.wait(timeout=3) + except subprocess.TimeoutExpired: + wifi_process.kill() + wifi_process = None + return jsonify({'status': 'stopped'}) + return jsonify({'status': 'not_running'}) + + +@app.route('/wifi/deauth', methods=['POST']) +def send_deauth(): + """Send deauthentication packets to force handshake capture.""" + data = request.json + target_bssid = data.get('bssid') + target_client = data.get('client', 'FF:FF:FF:FF:FF:FF') # Broadcast by default + count = data.get('count', 5) + interface = data.get('interface') or wifi_monitor_interface + + if not target_bssid: + return jsonify({'status': 'error', 'message': 'Target BSSID required'}) + + if not interface: + return jsonify({'status': 'error', 'message': 'No monitor interface'}) + + if not check_tool('aireplay-ng'): + return jsonify({'status': 'error', 'message': 'aireplay-ng not found'}) + + try: + # aireplay-ng --deauth -a -c + cmd = [ + 'aireplay-ng', + '--deauth', str(count), + '-a', target_bssid, + '-c', target_client, + interface + ] + + wifi_queue.put({'type': 'info', 'text': f'Sending {count} deauth packets to {target_bssid}'}) + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + if result.returncode == 0: + return jsonify({'status': 'success', 'message': f'Sent {count} deauth packets'}) + else: + return jsonify({'status': 'error', 'message': result.stderr}) + + except subprocess.TimeoutExpired: + return jsonify({'status': 'success', 'message': 'Deauth sent (timed out waiting for completion)'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/wifi/handshake/capture', methods=['POST']) +def capture_handshake(): + """Start targeted handshake capture.""" + global wifi_process + + data = request.json + target_bssid = data.get('bssid') + channel = data.get('channel') + interface = data.get('interface') or wifi_monitor_interface + + if not target_bssid or not channel: + return jsonify({'status': 'error', 'message': 'BSSID and channel required'}) + + with wifi_lock: + if wifi_process: + return jsonify({'status': 'error', 'message': 'Scan already running. Stop it first.'}) + + capture_path = f'/tmp/intercept_handshake_{target_bssid.replace(":", "")}' + + cmd = [ + 'airodump-ng', + '-c', str(channel), + '--bssid', target_bssid, + '-w', capture_path, + '--output-format', 'pcap', + interface + ] + + try: + wifi_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + wifi_queue.put({'type': 'info', 'text': f'Capturing handshakes for {target_bssid} on channel {channel}'}) + return jsonify({'status': 'started', 'capture_file': capture_path + '-01.cap'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/wifi/kismet/start', methods=['POST']) +def start_kismet(): + """Start Kismet for passive reconnaissance.""" + global kismet_process + + data = request.json + interface = data.get('interface') or wifi_monitor_interface + + if not interface: + return jsonify({'status': 'error', 'message': 'No interface specified'}) + + if not check_tool('kismet'): + return jsonify({'status': 'error', 'message': 'Kismet not found. Install with: brew install kismet'}) + + with wifi_lock: + if kismet_process: + return jsonify({'status': 'error', 'message': 'Kismet already running'}) + + try: + # Start Kismet with REST API enabled + cmd = [ + 'kismet', + '-c', interface, + '--no-ncurses', + '--override', 'httpd_bind_address=127.0.0.1', + '--override', 'httpd_port=2501' + ] + + kismet_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + wifi_queue.put({'type': 'info', 'text': 'Kismet started. API available at http://127.0.0.1:2501'}) + return jsonify({'status': 'started', 'api_url': 'http://127.0.0.1:2501'}) + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/wifi/kismet/stop', methods=['POST']) +def stop_kismet(): + """Stop Kismet.""" + global kismet_process + + with wifi_lock: + if kismet_process: + kismet_process.terminate() + try: + kismet_process.wait(timeout=5) + except subprocess.TimeoutExpired: + kismet_process.kill() + kismet_process = None + return jsonify({'status': 'stopped'}) + return jsonify({'status': 'not_running'}) + + +@app.route('/wifi/kismet/devices') +def get_kismet_devices(): + """Get devices from Kismet REST API.""" + import urllib.request + import json as json_module + + try: + # Kismet REST API endpoint for devices + url = 'http://127.0.0.1:2501/devices/views/all/devices.json' + req = urllib.request.Request(url) + req.add_header('KISMET', 'admin:admin') # Default credentials + + with urllib.request.urlopen(req, timeout=5) as response: + data = json_module.loads(response.read().decode()) + return jsonify({'devices': data}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/wifi/networks') +def get_wifi_networks(): + """Get current list of discovered networks.""" + return jsonify({ + 'networks': list(wifi_networks.values()), + 'clients': list(wifi_clients.values()), + 'handshakes': wifi_handshakes, + 'monitor_interface': wifi_monitor_interface + }) + + +@app.route('/wifi/stream') +def stream_wifi(): + """SSE stream for WiFi events.""" + def generate(): + import json + while True: + try: + msg = wifi_queue.get(timeout=1) + yield f"data: {json.dumps(msg)}\n\n" + except queue.Empty: + yield f"data: {json.dumps({'type': 'keepalive'})}\n\n" + + response = Response(generate(), mimetype='text/event-stream') + response.headers['Cache-Control'] = 'no-cache' + response.headers['X-Accel-Buffering'] = 'no' + response.headers['Connection'] = 'keep-alive' + return response + + +# ============== BLUETOOTH RECONNAISSANCE ROUTES ============== + +def get_manufacturer(mac): + """Look up manufacturer from MAC address OUI.""" + prefix = mac[:8].upper() + return OUI_DATABASE.get(prefix, 'Unknown') + + +def classify_bt_device(name, device_class, services): + """Classify Bluetooth device type based on available info.""" + name_lower = (name or '').lower() + + # Check name for common patterns + if any(x in name_lower for x in ['airpod', 'earbud', 'headphone', 'speaker', 'audio', 'beats', 'bose', 'jbl', 'sony wh', 'sony wf']): + return 'audio' + if any(x in name_lower for x in ['watch', 'band', 'fitbit', 'garmin', 'mi band']): + return 'wearable' + if any(x in name_lower for x in ['iphone', 'galaxy', 'pixel', 'phone', 'android']): + return 'phone' + if any(x in name_lower for x in ['airtag', 'tile', 'smarttag', 'tracker', 'chipolo']): + return 'tracker' + if any(x in name_lower for x in ['keyboard', 'mouse', 'controller', 'gamepad']): + return 'input' + if any(x in name_lower for x in ['tv', 'roku', 'chromecast', 'firestick']): + return 'media' + + # Check device class if available + if device_class: + major_class = (device_class >> 8) & 0x1F + if major_class == 1: # Computer + return 'computer' + elif major_class == 2: # Phone + return 'phone' + elif major_class == 4: # Audio/Video + return 'audio' + elif major_class == 5: # Peripheral + return 'input' + elif major_class == 6: # Imaging + return 'imaging' + elif major_class == 7: # Wearable + return 'wearable' + + return 'other' + + +def detect_tracker(mac, name, manufacturer_data=None): + """Detect if device is a known tracker (AirTag, Tile, etc).""" + mac_prefix = mac[:5].upper() + + # AirTag detection (Apple Find My) + if any(mac_prefix.startswith(p) for p in AIRTAG_PREFIXES): + if manufacturer_data and b'\\x4c\\x00' in manufacturer_data: + return {'type': 'airtag', 'name': 'Apple AirTag', 'risk': 'high'} + + # Tile detection + if any(mac_prefix.startswith(p) for p in TILE_PREFIXES): + return {'type': 'tile', 'name': 'Tile Tracker', 'risk': 'medium'} + + # Samsung SmartTag + if any(mac_prefix.startswith(p) for p in SAMSUNG_TRACKER): + return {'type': 'smarttag', 'name': 'Samsung SmartTag', 'risk': 'medium'} + + # Name-based detection + name_lower = (name or '').lower() + if 'airtag' in name_lower: + return {'type': 'airtag', 'name': 'Apple AirTag', 'risk': 'high'} + if 'tile' in name_lower: + return {'type': 'tile', 'name': 'Tile Tracker', 'risk': 'medium'} + if 'smarttag' in name_lower: + return {'type': 'smarttag', 'name': 'Samsung SmartTag', 'risk': 'medium'} + if 'chipolo' in name_lower: + return {'type': 'chipolo', 'name': 'Chipolo Tracker', 'risk': 'medium'} + + return None + + +def detect_bt_interfaces(): + """Detect available Bluetooth interfaces.""" + interfaces = [] + import platform + + if platform.system() == 'Linux': + try: + # Use hciconfig to list interfaces + result = subprocess.run(['hciconfig'], capture_output=True, text=True, timeout=5) + lines = result.stdout.split('\n') + current_iface = None + for line in lines: + if line and not line.startswith('\t') and not line.startswith(' '): + parts = line.split(':') + if parts: + current_iface = parts[0].strip() + interfaces.append({ + 'name': current_iface, + 'type': 'hci', + 'status': 'up' if 'UP' in line else 'down' + }) + except FileNotFoundError: + pass + except Exception as e: + print(f"[BT] Error detecting interfaces: {e}") + + elif platform.system() == 'Darwin': # macOS + # macOS uses different Bluetooth stack + interfaces.append({ + 'name': 'default', + 'type': 'macos', + 'status': 'available' + }) + + # Check for Ubertooth + try: + result = subprocess.run(['ubertooth-util', '-v'], capture_output=True, timeout=5) + if result.returncode == 0: + interfaces.append({ + 'name': 'ubertooth0', + 'type': 'ubertooth', + 'status': 'connected' + }) + except: + pass + + return interfaces + + +@app.route('/bt/interfaces') +def get_bt_interfaces(): + """Get available Bluetooth interfaces and tools.""" + interfaces = detect_bt_interfaces() + tools = { + 'hcitool': check_tool('hcitool'), + 'bluetoothctl': check_tool('bluetoothctl'), + 'ubertooth': check_tool('ubertooth-scan'), + 'bettercap': check_tool('bettercap'), + 'hciconfig': check_tool('hciconfig'), + 'l2ping': check_tool('l2ping'), + 'sdptool': check_tool('sdptool') + } + return jsonify({ + 'interfaces': interfaces, + 'tools': tools, + 'current_interface': bt_interface + }) + + +def parse_hcitool_output(line): + """Parse hcitool scan output line.""" + # Format: "AA:BB:CC:DD:EE:FF Device Name" + parts = line.strip().split('\t') + if len(parts) >= 2: + mac = parts[0].strip() + name = parts[1].strip() if len(parts) > 1 else '' + if ':' in mac and len(mac) == 17: + return {'mac': mac, 'name': name} + return None + + +def stream_bt_scan(process, scan_mode): + """Stream Bluetooth scan output to queue.""" + global bt_process, bt_devices + import time + + try: + bt_queue.put({'type': 'status', 'text': 'started'}) + + if scan_mode == 'hcitool': + # hcitool lescan output + for line in iter(process.stdout.readline, b''): + line = line.decode('utf-8', errors='replace').strip() + if not line or 'LE Scan' in line: + continue + + # Parse BLE device + parts = line.split() + if len(parts) >= 1 and ':' in parts[0]: + mac = parts[0] + name = ' '.join(parts[1:]) if len(parts) > 1 else '' + + device = { + 'mac': mac, + 'name': name or '[Unknown]', + 'manufacturer': get_manufacturer(mac), + 'type': classify_bt_device(name, None, None), + 'rssi': None, + 'last_seen': time.time() + } + + # Check for tracker + tracker = detect_tracker(mac, name) + if tracker: + device['tracker'] = tracker + + is_new = mac not in bt_devices + bt_devices[mac] = device + + bt_queue.put({ + 'type': 'device', + 'action': 'new' if is_new else 'update', + **device + }) + + elif scan_mode == 'bluetoothctl': + # bluetoothctl scan output + for line in iter(process.stdout.readline, b''): + line = line.decode('utf-8', errors='replace').strip() + + # Parse [NEW] Device or [CHG] Device lines + if 'Device' in line and ':' in line: + import re + match = re.search(r'([0-9A-Fa-f:]{17})\s*(.*)', line) + if match: + mac = match.group(1) + name = match.group(2).strip() + + device = { + 'mac': mac, + 'name': name or '[Unknown]', + 'manufacturer': get_manufacturer(mac), + 'type': classify_bt_device(name, None, None), + 'rssi': None, + 'last_seen': time.time() + } + + tracker = detect_tracker(mac, name) + if tracker: + device['tracker'] = tracker + + is_new = mac not in bt_devices + bt_devices[mac] = device + + bt_queue.put({ + 'type': 'device', + 'action': 'new' if is_new else 'update', + **device + }) + + except Exception as e: + bt_queue.put({'type': 'error', 'text': str(e)}) + finally: + process.wait() + bt_queue.put({'type': 'status', 'text': 'stopped'}) + with bt_lock: + bt_process = None + + +@app.route('/bt/scan/start', methods=['POST']) +def start_bt_scan(): + """Start Bluetooth scanning.""" + global bt_process, bt_devices, bt_interface + + with bt_lock: + if bt_process: + return jsonify({'status': 'error', 'message': 'Scan already running'}) + + data = request.json + scan_mode = data.get('mode', 'hcitool') + interface = data.get('interface', 'hci0') + duration = data.get('duration', 30) + scan_ble = data.get('scan_ble', True) + scan_classic = data.get('scan_classic', True) + + bt_interface = interface + bt_devices = {} + + # Clear queue + while not bt_queue.empty(): + try: + bt_queue.get_nowait() + except: + break + + try: + if scan_mode == 'hcitool': + if scan_ble: + cmd = ['hcitool', '-i', interface, 'lescan', '--duplicates'] + else: + cmd = ['hcitool', '-i', interface, 'scan'] + + bt_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + elif scan_mode == 'bluetoothctl': + # Use bluetoothctl for scanning + cmd = ['bluetoothctl'] + bt_process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + # Send scan on command + bt_process.stdin.write(b'scan on\n') + bt_process.stdin.flush() + + elif scan_mode == 'ubertooth': + cmd = ['ubertooth-scan', '-t', str(duration)] + bt_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + elif scan_mode == 'bettercap': + cmd = ['bettercap', '-eval', 'ble.recon on', '-silent'] + bt_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + else: + return jsonify({'status': 'error', 'message': f'Unknown scan mode: {scan_mode}'}) + + # Start streaming thread + thread = threading.Thread(target=stream_bt_scan, args=(bt_process, scan_mode)) + thread.daemon = True + thread.start() + + bt_queue.put({'type': 'info', 'text': f'Started {scan_mode} scan on {interface}'}) + return jsonify({'status': 'started', 'mode': scan_mode, 'interface': interface}) + + except FileNotFoundError as e: + return jsonify({'status': 'error', 'message': f'Tool not found: {e.filename}'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/bt/scan/stop', methods=['POST']) +def stop_bt_scan(): + """Stop Bluetooth scanning.""" + global bt_process + + with bt_lock: + if bt_process: + bt_process.terminate() + try: + bt_process.wait(timeout=3) + except subprocess.TimeoutExpired: + bt_process.kill() + bt_process = None + return jsonify({'status': 'stopped'}) + return jsonify({'status': 'not_running'}) + + +@app.route('/bt/enum', methods=['POST']) +def enum_bt_services(): + """Enumerate services on a Bluetooth device.""" + data = request.json + target_mac = data.get('mac') + + if not target_mac: + return jsonify({'status': 'error', 'message': 'Target MAC required'}) + + try: + # Try sdptool for classic BT + result = subprocess.run( + ['sdptool', 'browse', target_mac], + capture_output=True, text=True, timeout=30 + ) + + services = [] + current_service = {} + + for line in result.stdout.split('\n'): + line = line.strip() + if line.startswith('Service Name:'): + if current_service: + services.append(current_service) + current_service = {'name': line.split(':', 1)[1].strip()} + elif line.startswith('Service Description:'): + current_service['description'] = line.split(':', 1)[1].strip() + elif line.startswith('Service Provider:'): + current_service['provider'] = line.split(':', 1)[1].strip() + elif 'Protocol Descriptor' in line: + current_service['protocol'] = line + + if current_service: + services.append(current_service) + + bt_services[target_mac] = services + + return jsonify({ + 'status': 'success', + 'mac': target_mac, + 'services': services + }) + + except subprocess.TimeoutExpired: + return jsonify({'status': 'error', 'message': 'Connection timed out'}) + except FileNotFoundError: + return jsonify({'status': 'error', 'message': 'sdptool not found'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/bt/ping', methods=['POST']) +def ping_bt_device(): + """Ping a Bluetooth device using l2ping.""" + data = request.json + target_mac = data.get('mac') + count = data.get('count', 5) + + if not target_mac: + return jsonify({'status': 'error', 'message': 'Target MAC required'}) + + try: + result = subprocess.run( + ['l2ping', '-c', str(count), target_mac], + capture_output=True, text=True, timeout=30 + ) + + return jsonify({ + 'status': 'success', + 'output': result.stdout, + 'reachable': result.returncode == 0 + }) + + except subprocess.TimeoutExpired: + return jsonify({'status': 'error', 'message': 'Ping timed out'}) + except FileNotFoundError: + return jsonify({'status': 'error', 'message': 'l2ping not found'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/bt/dos', methods=['POST']) +def dos_bt_device(): + """Flood ping a Bluetooth device (DoS test).""" + data = request.json + target_mac = data.get('mac') + count = data.get('count', 100) + size = data.get('size', 600) + + if not target_mac: + return jsonify({'status': 'error', 'message': 'Target MAC required'}) + + try: + # l2ping flood with large packets + result = subprocess.run( + ['l2ping', '-c', str(count), '-s', str(size), '-f', target_mac], + capture_output=True, text=True, timeout=60 + ) + + bt_queue.put({'type': 'info', 'text': f'DoS test complete on {target_mac}'}) + + return jsonify({ + 'status': 'success', + 'output': result.stdout + }) + + except subprocess.TimeoutExpired: + return jsonify({'status': 'success', 'message': 'DoS test timed out (expected)'}) + except FileNotFoundError: + return jsonify({'status': 'error', 'message': 'l2ping not found'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}) + + +@app.route('/bt/devices') +def get_bt_devices(): + """Get current list of discovered Bluetooth devices.""" + return jsonify({ + 'devices': list(bt_devices.values()), + 'beacons': list(bt_beacons.values()), + 'interface': bt_interface + }) + + +@app.route('/bt/stream') +def stream_bt(): + """SSE stream for Bluetooth events.""" + def generate(): + import json + while True: + try: + msg = bt_queue.get(timeout=1) + yield f"data: {json.dumps(msg)}\n\n" + except queue.Empty: + yield f"data: {json.dumps({'type': 'keepalive'})}\n\n" + + response = Response(generate(), mimetype='text/event-stream') + response.headers['Cache-Control'] = 'no-cache' + response.headers['X-Accel-Buffering'] = 'no' + response.headers['Connection'] = 'keep-alive' + return response + + def main(): print("=" * 50) print(" INTERCEPT // Signal Intelligence") - print(" POCSAG / FLEX / 433MHz Sensors") + print(" POCSAG / FLEX / 433MHz / WiFi / Bluetooth") print("=" * 50) print() print("Open http://localhost:5050 in your browser")