mirror of
https://github.com/smittix/intercept.git
synced 2026-07-28 10:38:12 -07:00
Add TSCM support to distributed agent with local mode parity
- Agent TSCM uses same ThreatDetector and CorrelationEngine as local mode - Added baseline_id parameter support using get_tscm_baseline() - Fixed RF scan stop_check to allow agent-specific stop events - Fixed 'undefined MHz' display for WiFi devices (added essid fallback and null check) - Fixed signal strength type conversion (string to int) for correlation engine - Agent threat detection matches local mode behavior: - No baseline: detects anomaly/hidden_camera threats only - With baseline: also detects new_device threats
This commit is contained in:
+719
-200
File diff suppressed because it is too large
Load Diff
+14
-3
@@ -153,10 +153,16 @@ def get_agent_detail(agent_id: int):
|
|||||||
client = create_client_from_agent(agent)
|
client = create_client_from_agent(agent)
|
||||||
metadata = client.refresh_metadata()
|
metadata = client.refresh_metadata()
|
||||||
if metadata['healthy']:
|
if metadata['healthy']:
|
||||||
|
caps = metadata['capabilities'] or {}
|
||||||
|
# Store full interfaces structure (wifi, bt, sdr)
|
||||||
|
agent_interfaces = caps.get('interfaces', {})
|
||||||
|
# Fallback: also include top-level devices for backwards compatibility
|
||||||
|
if not agent_interfaces.get('sdr_devices') and caps.get('devices'):
|
||||||
|
agent_interfaces['sdr_devices'] = caps.get('devices', [])
|
||||||
update_agent(
|
update_agent(
|
||||||
agent_id,
|
agent_id,
|
||||||
capabilities=metadata['capabilities'].get('modes') if metadata['capabilities'] else None,
|
capabilities=caps.get('modes'),
|
||||||
interfaces={'devices': metadata['capabilities'].get('devices', [])} if metadata['capabilities'] else None,
|
interfaces=agent_interfaces,
|
||||||
update_last_seen=True
|
update_last_seen=True
|
||||||
)
|
)
|
||||||
agent = get_agent(agent_id)
|
agent = get_agent(agent_id)
|
||||||
@@ -215,10 +221,15 @@ def refresh_agent_metadata(agent_id: int):
|
|||||||
|
|
||||||
if metadata['healthy']:
|
if metadata['healthy']:
|
||||||
caps = metadata['capabilities'] or {}
|
caps = metadata['capabilities'] or {}
|
||||||
|
# Store full interfaces structure (wifi, bt, sdr)
|
||||||
|
agent_interfaces = caps.get('interfaces', {})
|
||||||
|
# Fallback: also include top-level devices for backwards compatibility
|
||||||
|
if not agent_interfaces.get('sdr_devices') and caps.get('devices'):
|
||||||
|
agent_interfaces['sdr_devices'] = caps.get('devices', [])
|
||||||
update_agent(
|
update_agent(
|
||||||
agent_id,
|
agent_id,
|
||||||
capabilities=caps.get('modes'),
|
capabilities=caps.get('modes'),
|
||||||
interfaces={'devices': caps.get('devices', [])},
|
interfaces=agent_interfaces,
|
||||||
update_last_seen=True
|
update_last_seen=True
|
||||||
)
|
)
|
||||||
agent = get_agent(agent_id)
|
agent = get_agent(agent_id)
|
||||||
|
|||||||
+11
-2
@@ -944,7 +944,7 @@ def _scan_bluetooth_devices(interface: str, duration: int = 10) -> list[dict]:
|
|||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
def _scan_rf_signals(sdr_device: int | None, duration: int = 30) -> list[dict]:
|
def _scan_rf_signals(sdr_device: int | None, duration: int = 30, stop_check: callable | None = None) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Scan for RF signals using SDR (rtl_power).
|
Scan for RF signals using SDR (rtl_power).
|
||||||
|
|
||||||
@@ -956,7 +956,16 @@ def _scan_rf_signals(sdr_device: int | None, duration: int = 30) -> list[dict]:
|
|||||||
- 915 MHz: US ISM band
|
- 915 MHz: US ISM band
|
||||||
- 1.2 GHz: Video transmitters
|
- 1.2 GHz: Video transmitters
|
||||||
- 2.4 GHz: WiFi, Bluetooth, video transmitters
|
- 2.4 GHz: WiFi, Bluetooth, video transmitters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sdr_device: SDR device index
|
||||||
|
duration: Scan duration per band
|
||||||
|
stop_check: Optional callable that returns True if scan should stop.
|
||||||
|
Defaults to checking module-level _sweep_running.
|
||||||
"""
|
"""
|
||||||
|
# Default stop check uses module-level _sweep_running
|
||||||
|
if stop_check is None:
|
||||||
|
stop_check = lambda: not _sweep_running
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -1021,7 +1030,7 @@ def _scan_rf_signals(sdr_device: int | None, duration: int = 30) -> list[dict]:
|
|||||||
|
|
||||||
# Scan each band and look for strong signals
|
# Scan each band and look for strong signals
|
||||||
for start_freq, end_freq, bin_size, band_name in scan_bands:
|
for start_freq, end_freq, bin_size, band_name in scan_bands:
|
||||||
if not _sweep_running:
|
if stop_check():
|
||||||
break
|
break
|
||||||
|
|
||||||
logger.info(f"Scanning {band_name} ({start_freq/1e6:.1f}-{end_freq/1e6:.1f} MHz)")
|
logger.info(f"Scanning {band_name} ({start_freq/1e6:.1f}-{end_freq/1e6:.1f} MHz)")
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ function updateAgentHealthUI() {
|
|||||||
const selector = document.getElementById('agentSelect');
|
const selector = document.getElementById('agentSelect');
|
||||||
if (!selector) return;
|
if (!selector) return;
|
||||||
|
|
||||||
// Update each option in selector
|
// Update each option in selector with status and latency
|
||||||
agents.forEach(agent => {
|
agents.forEach(agent => {
|
||||||
const option = selector.querySelector(`option[value="${agent.id}"]`);
|
const option = selector.querySelector(`option[value="${agent.id}"]`);
|
||||||
if (option) {
|
if (option) {
|
||||||
|
|||||||
+114
-34
@@ -2231,7 +2231,7 @@
|
|||||||
|
|
||||||
// Show agent selector for modes that support remote agents
|
// Show agent selector for modes that support remote agents
|
||||||
const agentSection = document.getElementById('agentSection');
|
const agentSection = document.getElementById('agentSection');
|
||||||
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft'];
|
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais', 'acars', 'dsc'];
|
||||||
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
|
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
|
||||||
|
|
||||||
// Show RTL-SDR device section for modes that use it
|
// Show RTL-SDR device section for modes that use it
|
||||||
@@ -8943,6 +8943,15 @@
|
|||||||
const btIndicator = document.getElementById('tscmBtIndicator');
|
const btIndicator = document.getElementById('tscmBtIndicator');
|
||||||
const rfIndicator = document.getElementById('tscmRfIndicator');
|
const rfIndicator = document.getElementById('tscmRfIndicator');
|
||||||
|
|
||||||
|
// Safety check for agent mode which may not return devices
|
||||||
|
if (!devices) {
|
||||||
|
// Just mark all as active if we don't have device info
|
||||||
|
if (wifiIndicator) wifiIndicator.classList.add('active');
|
||||||
|
if (btIndicator) btIndicator.classList.add('active');
|
||||||
|
if (rfIndicator) rfIndicator.classList.add('active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (wifiIndicator) {
|
if (wifiIndicator) {
|
||||||
wifiIndicator.classList.toggle('active', devices.wifi);
|
wifiIndicator.classList.toggle('active', devices.wifi);
|
||||||
wifiIndicator.classList.toggle('inactive', !devices.wifi);
|
wifiIndicator.classList.toggle('inactive', !devices.wifi);
|
||||||
@@ -8975,6 +8984,10 @@
|
|||||||
tscmEventSource.close();
|
tscmEventSource.close();
|
||||||
tscmEventSource = null;
|
tscmEventSource = null;
|
||||||
}
|
}
|
||||||
|
if (typeof tscmAgentPollInterval !== 'undefined' && tscmAgentPollInterval) {
|
||||||
|
clearInterval(tscmAgentPollInterval);
|
||||||
|
tscmAgentPollInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('startTscmBtn').style.display = 'block';
|
document.getElementById('startTscmBtn').style.display = 'block';
|
||||||
document.getElementById('stopTscmBtn').style.display = 'none';
|
document.getElementById('stopTscmBtn').style.display = 'none';
|
||||||
@@ -9518,46 +9531,113 @@
|
|||||||
reportWindow.document.close();
|
reportWindow.document.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tscmAgentPollInterval = null;
|
||||||
|
|
||||||
function startTscmStream() {
|
function startTscmStream() {
|
||||||
if (tscmEventSource) {
|
if (tscmEventSource) {
|
||||||
tscmEventSource.close();
|
tscmEventSource.close();
|
||||||
|
tscmEventSource = null;
|
||||||
|
}
|
||||||
|
if (tscmAgentPollInterval) {
|
||||||
|
clearInterval(tscmAgentPollInterval);
|
||||||
|
tscmAgentPollInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if using agent - connect to multi-agent stream
|
// Check if using agent
|
||||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
const streamUrl = isAgentMode
|
|
||||||
? '/controller/stream/all'
|
|
||||||
: '/tscm/sweep/stream';
|
|
||||||
|
|
||||||
tscmEventSource = new EventSource(streamUrl);
|
if (isAgentMode) {
|
||||||
|
// For agent mode, poll the agent for TSCM data since push may not be enabled
|
||||||
|
console.log('[TSCM] Starting agent polling mode');
|
||||||
|
pollAgentTscmData(); // Initial poll
|
||||||
|
tscmAgentPollInterval = setInterval(pollAgentTscmData, 2000); // Poll every 2 seconds
|
||||||
|
} else {
|
||||||
|
// For local mode, use SSE stream
|
||||||
|
const streamUrl = '/tscm/sweep/stream';
|
||||||
|
tscmEventSource = new EventSource(streamUrl);
|
||||||
|
|
||||||
tscmEventSource.onmessage = function (event) {
|
tscmEventSource.onmessage = function (event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
// If using multi-agent stream, filter for TSCM data
|
|
||||||
if (isAgentMode) {
|
|
||||||
if (data.scan_type === 'tscm' || data.type?.startsWith('tscm') ||
|
|
||||||
data.type === 'wifi_device' || data.type === 'bt_device' ||
|
|
||||||
data.type === 'rf_signal' || data.type === 'threat' ||
|
|
||||||
data.type === 'sweep_progress') {
|
|
||||||
// Add agent info to data for display
|
|
||||||
if (data.agent_name) {
|
|
||||||
data._agent = data.agent_name;
|
|
||||||
}
|
|
||||||
handleTscmEvent(data.payload || data);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleTscmEvent(data);
|
handleTscmEvent(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('TSCM SSE parse error:', e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
};
|
||||||
console.error('TSCM SSE parse error:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tscmEventSource.onerror = function () {
|
tscmEventSource.onerror = function () {
|
||||||
console.warn('TSCM SSE connection error');
|
console.warn('TSCM SSE connection error');
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollAgentTscmData() {
|
||||||
|
if (!isTscmRunning) {
|
||||||
|
if (tscmAgentPollInterval) {
|
||||||
|
clearInterval(tscmAgentPollInterval);
|
||||||
|
tscmAgentPollInterval = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/controller/agents/${currentAgent}/tscm/data`);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.status === 'success' && result.data) {
|
||||||
|
// Agent data is nested: result.data.data (controller wraps agent response)
|
||||||
|
const data = result.data.data || result.data;
|
||||||
|
|
||||||
|
// Process WiFi devices
|
||||||
|
if (data.wifi_devices) {
|
||||||
|
data.wifi_devices.forEach(device => {
|
||||||
|
if (!tscmWifiDevices.find(d => d.bssid === device.bssid)) {
|
||||||
|
handleTscmEvent({ type: 'wifi_device', ...device });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process Bluetooth devices
|
||||||
|
if (data.bt_devices) {
|
||||||
|
data.bt_devices.forEach(device => {
|
||||||
|
if (!tscmBtDevices.find(d => d.address === device.address)) {
|
||||||
|
handleTscmEvent({ type: 'bt_device', ...device });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process anomalies/threats
|
||||||
|
// Agent now uses same ThreatDetector as local mode, so format matches:
|
||||||
|
// threat_type, severity, source, identifier, name, signal_strength
|
||||||
|
if (data.anomalies) {
|
||||||
|
data.anomalies.forEach(threat => {
|
||||||
|
handleTscmEvent({
|
||||||
|
type: 'threat_detected',
|
||||||
|
...threat
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process RF signals
|
||||||
|
if (data.rf_signals) {
|
||||||
|
data.rf_signals.forEach(signal => {
|
||||||
|
handleTscmEvent({ type: 'rf_signal', ...signal });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress (simple time-based estimate)
|
||||||
|
if (tscmSweepStartTime) {
|
||||||
|
const elapsed = (Date.now() - tscmSweepStartTime) / 1000;
|
||||||
|
const sweepType = document.getElementById('tscmSweepType')?.value || 'standard';
|
||||||
|
const durations = { quick: 120, standard: 300, full: 900 };
|
||||||
|
const maxDuration = durations[sweepType] || 300;
|
||||||
|
const progress = Math.min(95, (elapsed / maxDuration) * 100);
|
||||||
|
updateTscmProgress({ progress: Math.round(progress), phase: 'Scanning' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[TSCM] Agent poll error:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tscmCorrelations = [];
|
let tscmCorrelations = [];
|
||||||
@@ -9714,7 +9794,7 @@
|
|||||||
tscmHighInterestDevices.push({
|
tscmHighInterestDevices.push({
|
||||||
id: id,
|
id: id,
|
||||||
protocol: protocol,
|
protocol: protocol,
|
||||||
name: device.name || device.ssid || `${device.frequency} MHz`,
|
name: device.name || device.essid || device.ssid || (device.frequency ? `${device.frequency.toFixed(3)} MHz` : 'Unknown Device'),
|
||||||
score: device.score,
|
score: device.score,
|
||||||
classification: device.classification,
|
classification: device.classification,
|
||||||
indicators: device.indicators || [],
|
indicators: device.indicators || [],
|
||||||
@@ -9811,7 +9891,7 @@
|
|||||||
document.getElementById('tscmInformationalCard').classList.toggle('active', counts.informational > 0);
|
document.getElementById('tscmInformationalCard').classList.toggle('active', counts.informational > 0);
|
||||||
document.getElementById('tscmCorrelationsCard').classList.toggle('active', tscmCorrelations.length > 0);
|
document.getElementById('tscmCorrelationsCard').classList.toggle('active', tscmCorrelations.length > 0);
|
||||||
|
|
||||||
// Update threat panel count (now shows high interest items)
|
// Update threat panel count (shows high interest items only)
|
||||||
document.getElementById('tscmThreatCount').textContent = counts.high_interest;
|
document.getElementById('tscmThreatCount').textContent = counts.high_interest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9873,7 +9953,7 @@
|
|||||||
// Build detailed view
|
// Build detailed view
|
||||||
let html = `
|
let html = `
|
||||||
<div class="device-detail-header ${getClassificationClass(device.classification)}">
|
<div class="device-detail-header ${getClassificationClass(device.classification)}">
|
||||||
<h3>${getClassificationIcon(device.classification)} ${escapeHtml(device.name || device.ssid || device.mac || device.bssid || device.frequency + ' MHz')}</h3>
|
<h3>${getClassificationIcon(device.classification)} ${escapeHtml(device.name || device.essid || device.ssid || device.mac || device.bssid || (device.frequency ? device.frequency.toFixed(3) + ' MHz' : 'Unknown'))}</h3>
|
||||||
<span class="device-detail-protocol">${protocol.toUpperCase()}</span>
|
<span class="device-detail-protocol">${protocol.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -10109,7 +10189,7 @@
|
|||||||
<div class="category-device-header">
|
<div class="category-device-header">
|
||||||
<span class="category-device-name">
|
<span class="category-device-name">
|
||||||
${getClassificationIcon(d.classification)}
|
${getClassificationIcon(d.classification)}
|
||||||
${escapeHtml(d.name || d.ssid || d.mac || d.bssid || d.frequency + ' MHz')}
|
${escapeHtml(d.name || d.ssid || d.mac || d.bssid || (d.frequency ? d.frequency.toFixed(3) + ' MHz' : 'Unknown'))}
|
||||||
</span>
|
</span>
|
||||||
<span class="category-device-score">${d.score || 0}</span>
|
<span class="category-device-score">${d.score || 0}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user