mirror of
https://github.com/smittix/intercept.git
synced 2026-07-23 16:38:11 -07:00
Fix TSCM sweep scan resilience and add per-device error isolation
The sweep loop's WiFi/BT/RF scan processing had unprotected timeline_manager.add_observation() calls that could crash an entire scan iteration, silently preventing all device events from reaching the frontend. Additionally, scan interval timestamps were only updated at the end of processing, causing tight retry loops on persistent errors. - Wrap timeline observation calls in try/except for all three protocols - Move last_*_scan timestamp updates immediately after scan completes - Add per-device try/except so one bad device doesn't block others - Emit sweep_progress after WiFi scan for real-time status visibility - Log warning when WiFi scan returns 0 networks for easier diagnosis - Add known_device and score_modifier fields to correlation engine - Add TSCM scheduling, cases, known devices, and advanced WiFi indicators Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+26
-3
@@ -3122,6 +3122,7 @@ class ModeManager:
|
|||||||
wifi_interface = params.get('wifi_interface') or params.get('interface')
|
wifi_interface = params.get('wifi_interface') or params.get('interface')
|
||||||
bt_adapter = params.get('bt_interface') or params.get('adapter', 'hci0')
|
bt_adapter = params.get('bt_interface') or params.get('adapter', 'hci0')
|
||||||
sdr_device = params.get('sdr_device', params.get('device', 0))
|
sdr_device = params.get('sdr_device', params.get('device', 0))
|
||||||
|
sweep_type = params.get('sweep_type')
|
||||||
|
|
||||||
# Get baseline_id for comparison (same as local mode)
|
# Get baseline_id for comparison (same as local mode)
|
||||||
baseline_id = params.get('baseline_id')
|
baseline_id = params.get('baseline_id')
|
||||||
@@ -3131,7 +3132,7 @@ class ModeManager:
|
|||||||
# Start the combined TSCM scanner thread using existing Intercept functions
|
# Start the combined TSCM scanner thread using existing Intercept functions
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=self._tscm_scanner_thread,
|
target=self._tscm_scanner_thread,
|
||||||
args=(scan_wifi, scan_bt, scan_rf, wifi_interface, bt_adapter, sdr_device, baseline_id),
|
args=(scan_wifi, scan_bt, scan_rf, wifi_interface, bt_adapter, sdr_device, baseline_id, sweep_type),
|
||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
@@ -3154,7 +3155,7 @@ class ModeManager:
|
|||||||
|
|
||||||
def _tscm_scanner_thread(self, scan_wifi: bool, scan_bt: bool, scan_rf: bool,
|
def _tscm_scanner_thread(self, scan_wifi: bool, scan_bt: bool, scan_rf: bool,
|
||||||
wifi_interface: str | None, bt_adapter: str, sdr_device: int,
|
wifi_interface: str | None, bt_adapter: str, sdr_device: int,
|
||||||
baseline_id: int | None = None):
|
baseline_id: int | None = None, sweep_type: str | None = None):
|
||||||
"""Combined TSCM scanner using existing Intercept functions.
|
"""Combined TSCM scanner using existing Intercept functions.
|
||||||
|
|
||||||
NOTE: This matches local mode behavior exactly:
|
NOTE: This matches local mode behavior exactly:
|
||||||
@@ -3170,6 +3171,15 @@ class ModeManager:
|
|||||||
from routes.tscm import _scan_wifi_networks, _scan_bluetooth_devices, _scan_rf_signals
|
from routes.tscm import _scan_wifi_networks, _scan_bluetooth_devices, _scan_rf_signals
|
||||||
logger.info("TSCM imports successful")
|
logger.info("TSCM imports successful")
|
||||||
|
|
||||||
|
sweep_ranges = None
|
||||||
|
if sweep_type:
|
||||||
|
try:
|
||||||
|
from data.tscm_frequencies import get_sweep_preset, SWEEP_PRESETS
|
||||||
|
preset = get_sweep_preset(sweep_type) or SWEEP_PRESETS.get('standard')
|
||||||
|
sweep_ranges = preset.get('ranges') if preset else None
|
||||||
|
except Exception:
|
||||||
|
sweep_ranges = None
|
||||||
|
|
||||||
# Load baseline if specified (same as local mode)
|
# Load baseline if specified (same as local mode)
|
||||||
baseline = None
|
baseline = None
|
||||||
if baseline_id and HAS_BASELINE_DB and get_tscm_baseline:
|
if baseline_id and HAS_BASELINE_DB and get_tscm_baseline:
|
||||||
@@ -3243,6 +3253,9 @@ class ModeManager:
|
|||||||
profile = self._tscm_correlation.analyze_wifi_device(enriched)
|
profile = self._tscm_correlation.analyze_wifi_device(enriched)
|
||||||
enriched['classification'] = profile.risk_level.value
|
enriched['classification'] = profile.risk_level.value
|
||||||
enriched['score'] = profile.total_score
|
enriched['score'] = profile.total_score
|
||||||
|
enriched['score_modifier'] = profile.score_modifier
|
||||||
|
enriched['known_device'] = profile.known_device
|
||||||
|
enriched['known_device_name'] = profile.known_device_name
|
||||||
enriched['indicators'] = [
|
enriched['indicators'] = [
|
||||||
{'type': i.type.value, 'desc': i.description}
|
{'type': i.type.value, 'desc': i.description}
|
||||||
for i in profile.indicators
|
for i in profile.indicators
|
||||||
@@ -3289,6 +3302,9 @@ class ModeManager:
|
|||||||
profile = self._tscm_correlation.analyze_bluetooth_device(enriched)
|
profile = self._tscm_correlation.analyze_bluetooth_device(enriched)
|
||||||
enriched['classification'] = profile.risk_level.value
|
enriched['classification'] = profile.risk_level.value
|
||||||
enriched['score'] = profile.total_score
|
enriched['score'] = profile.total_score
|
||||||
|
enriched['score_modifier'] = profile.score_modifier
|
||||||
|
enriched['known_device'] = profile.known_device
|
||||||
|
enriched['known_device_name'] = profile.known_device_name
|
||||||
enriched['indicators'] = [
|
enriched['indicators'] = [
|
||||||
{'type': i.type.value, 'desc': i.description}
|
{'type': i.type.value, 'desc': i.description}
|
||||||
for i in profile.indicators
|
for i in profile.indicators
|
||||||
@@ -3304,7 +3320,11 @@ class ModeManager:
|
|||||||
try:
|
try:
|
||||||
# Pass a stop check that uses our stop_event (not the module's _sweep_running)
|
# Pass a stop check that uses our stop_event (not the module's _sweep_running)
|
||||||
agent_stop_check = lambda: stop_event and stop_event.is_set()
|
agent_stop_check = lambda: stop_event and stop_event.is_set()
|
||||||
rf_signals = _scan_rf_signals(sdr_device, stop_check=agent_stop_check)
|
rf_signals = _scan_rf_signals(
|
||||||
|
sdr_device,
|
||||||
|
stop_check=agent_stop_check,
|
||||||
|
sweep_ranges=sweep_ranges
|
||||||
|
)
|
||||||
|
|
||||||
# Analyze each RF signal like local mode does
|
# Analyze each RF signal like local mode does
|
||||||
analyzed_signals = []
|
analyzed_signals = []
|
||||||
@@ -3328,6 +3348,9 @@ class ModeManager:
|
|||||||
profile = self._tscm_correlation.analyze_rf_signal(signal)
|
profile = self._tscm_correlation.analyze_rf_signal(signal)
|
||||||
analyzed['classification'] = profile.risk_level.value
|
analyzed['classification'] = profile.risk_level.value
|
||||||
analyzed['score'] = profile.total_score
|
analyzed['score'] = profile.total_score
|
||||||
|
analyzed['score_modifier'] = profile.score_modifier
|
||||||
|
analyzed['known_device'] = profile.known_device
|
||||||
|
analyzed['known_device_name'] = profile.known_device_name
|
||||||
analyzed['indicators'] = [
|
analyzed['indicators'] = [
|
||||||
{'type': i.type.value, 'desc': i.description}
|
{'type': i.type.value, 'desc': i.description}
|
||||||
for i in profile.indicators
|
for i in profile.indicators
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import time
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request, Response
|
from flask import Blueprint, jsonify, request, Response
|
||||||
|
|
||||||
from utils.database import (
|
from utils.database import (
|
||||||
@@ -480,6 +482,47 @@ def proxy_mode_data(agent_id: int, mode: str):
|
|||||||
}), 502
|
}), 502
|
||||||
|
|
||||||
|
|
||||||
|
@controller_bp.route('/agents/<int:agent_id>/<mode>/stream')
|
||||||
|
def proxy_mode_stream(agent_id: int, mode: str):
|
||||||
|
"""Proxy SSE stream from a remote agent."""
|
||||||
|
agent = get_agent(agent_id)
|
||||||
|
if not agent:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Agent not found'}), 404
|
||||||
|
|
||||||
|
client = create_client_from_agent(agent)
|
||||||
|
query = request.query_string.decode('utf-8')
|
||||||
|
url = f"{client.base_url}/{mode}/stream"
|
||||||
|
if query:
|
||||||
|
url = f"{url}?{query}"
|
||||||
|
|
||||||
|
headers = {'Accept': 'text/event-stream'}
|
||||||
|
if agent.get('api_key'):
|
||||||
|
headers['X-API-Key'] = agent['api_key']
|
||||||
|
|
||||||
|
def generate() -> Generator[str, None, None]:
|
||||||
|
try:
|
||||||
|
with requests.get(url, headers=headers, stream=True, timeout=(5, 3600)) as resp:
|
||||||
|
resp.raise_for_status()
|
||||||
|
for chunk in resp.iter_content(chunk_size=1024):
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
yield chunk.decode('utf-8', errors='ignore')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SSE proxy error for agent {agent_id}/{mode}: {e}")
|
||||||
|
yield format_sse({
|
||||||
|
'type': 'error',
|
||||||
|
'message': str(e),
|
||||||
|
'agent_id': agent_id,
|
||||||
|
'mode': mode,
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@controller_bp.route('/agents/<int:agent_id>/wifi/monitor', methods=['POST'])
|
@controller_bp.route('/agents/<int:agent_id>/wifi/monitor', methods=['POST'])
|
||||||
def proxy_wifi_monitor(agent_id: int):
|
def proxy_wifi_monitor(agent_id: int):
|
||||||
"""Toggle monitor mode on a remote agent's WiFi interface."""
|
"""Toggle monitor mode on a remote agent's WiFi interface."""
|
||||||
|
|||||||
+731
-134
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: rgba(0,0,0,0.3);
|
background: rgba(0,0,0,0.3);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.tscm-threat-banner .threat-card {
|
.tscm-threat-banner .threat-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -200,6 +201,17 @@
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
.known-badge {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(0, 255, 136, 0.2);
|
||||||
|
color: #00ff88;
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
.tscm-device-header {
|
.tscm-device-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -465,6 +477,18 @@
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
width: 40%;
|
width: 40%;
|
||||||
}
|
}
|
||||||
|
.device-detail-id {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
.tscm-more-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
.indicator-list {
|
.indicator-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -882,6 +906,42 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filters */
|
||||||
|
.tscm-filter-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.tscm-filter-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.tscm-filter-group label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
}
|
||||||
|
.tscm-filter-group select {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.tscm-filter-status {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* Advanced Modal Styles */
|
/* Advanced Modal Styles */
|
||||||
.tscm-advanced-modal {
|
.tscm-advanced-modal {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
@@ -1461,3 +1521,156 @@
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Meeting banner actions */
|
||||||
|
.tscm-meeting-banner {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tscm-meeting-banner .meeting-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case linking */
|
||||||
|
.tscm-case-link-banner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: rgba(74, 158, 255, 0.12);
|
||||||
|
border: 1px solid rgba(74, 158, 255, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.case-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.tscm-case-link-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(74, 158, 255, 0.2);
|
||||||
|
color: #9ed0ff;
|
||||||
|
border: 1px solid rgba(74, 158, 255, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Schedules */
|
||||||
|
.tscm-schedule-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.tscm-schedule-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.tscm-schedule-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.tscm-schedule-item.enabled {
|
||||||
|
border-color: rgba(0, 255, 136, 0.35);
|
||||||
|
}
|
||||||
|
.tscm-schedule-item.disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.tscm-schedule-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.tscm-schedule-status {
|
||||||
|
font-size: 9px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.tscm-schedule-meta {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.tscm-schedule-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Meeting summary */
|
||||||
|
.tscm-summary-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.tscm-summary-item {
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.tscm-summary-meta {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.tscm-summary-risk {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #ff9933;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case notes */
|
||||||
|
.tscm-case-notes {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.tscm-case-note {
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.tscm-case-note-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.tscm-case-note-type {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
.tscm-case-note-content {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.tscm-case-note-author {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.tscm-case-note-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.tscm-case-note-form textarea {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
.tscm-case-note-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -868,11 +868,8 @@ function connectAgentStream(mode, onMessage) {
|
|||||||
if (currentAgent === 'local') {
|
if (currentAgent === 'local') {
|
||||||
streamUrl = `/${mode}/stream`;
|
streamUrl = `/${mode}/stream`;
|
||||||
} else {
|
} else {
|
||||||
// For remote agents, we could either:
|
// For remote agents, proxy SSE through controller
|
||||||
// 1. Use the multi-agent stream: /controller/stream/all
|
streamUrl = `/controller/agents/${currentAgent}/${mode}/stream`;
|
||||||
// 2. Or proxy through controller (not implemented yet)
|
|
||||||
// For now, use multi-agent stream which includes agent_name tagging
|
|
||||||
streamUrl = '/controller/stream/all';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
agentEventSource = new EventSource(streamUrl);
|
agentEventSource = new EventSource(streamUrl);
|
||||||
@@ -881,14 +878,6 @@ function connectAgentStream(mode, onMessage) {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
// If using multi-agent stream, filter by current agent if needed
|
|
||||||
if (streamUrl === '/controller/stream/all' && currentAgent !== 'local') {
|
|
||||||
const agent = agents.find(a => a.id == currentAgent);
|
|
||||||
if (agent && data.agent_name && data.agent_name !== agent.name) {
|
|
||||||
return; // Skip messages from other agents
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMessage(data);
|
onMessage(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing SSE message:', e);
|
console.error('Error parsing SSE message:', e);
|
||||||
|
|||||||
@@ -879,6 +879,7 @@ const WiFiMode = (function() {
|
|||||||
updateNetworkRow(network);
|
updateNetworkRow(network);
|
||||||
updateStats();
|
updateStats();
|
||||||
updateProximityRadar();
|
updateProximityRadar();
|
||||||
|
updateChannelChart();
|
||||||
|
|
||||||
if (onNetworkUpdate) onNetworkUpdate(network);
|
if (onNetworkUpdate) onNetworkUpdate(network);
|
||||||
}
|
}
|
||||||
@@ -1420,9 +1421,15 @@ const WiFiMode = (function() {
|
|||||||
return Object.values(stats).filter(s => s.ap_count > 0 || [1, 6, 11, 36, 40, 44, 48, 149, 153, 157, 161, 165].includes(s.channel));
|
return Object.values(stats).filter(s => s.ap_count > 0 || [1, 6, 11, 36, 40, 44, 48, 149, 153, 157, 161, 165].includes(s.channel));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChannelChart(band = '2.4') {
|
function updateChannelChart(band) {
|
||||||
if (typeof ChannelChart === 'undefined') return;
|
if (typeof ChannelChart === 'undefined') return;
|
||||||
|
|
||||||
|
// Use the currently active band tab if no band specified
|
||||||
|
if (!band) {
|
||||||
|
const activeTab = elements.channelBandTabs && elements.channelBandTabs.querySelector('.channel-band-tab.active');
|
||||||
|
band = activeTab ? activeTab.dataset.band : '2.4';
|
||||||
|
}
|
||||||
|
|
||||||
// Recalculate channel stats from networks if needed
|
// Recalculate channel stats from networks if needed
|
||||||
if (channelStats.length === 0 && networks.size > 0) {
|
if (channelStats.length === 0 && networks.size > 0) {
|
||||||
channelStats = calculateChannelStats();
|
channelStats = calculateChannelStats();
|
||||||
|
|||||||
+1624
-54
File diff suppressed because it is too large
Load Diff
@@ -146,6 +146,9 @@
|
|||||||
<button class="stop-btn" id="tscmEndMeetingBtn" onclick="tscmEndMeeting()" style="width: 100%; padding: 8px; display: none;">
|
<button class="stop-btn" id="tscmEndMeetingBtn" onclick="tscmEndMeeting()" style="width: 100%; padding: 8px; display: none;">
|
||||||
End Meeting Window
|
End Meeting Window
|
||||||
</button>
|
</button>
|
||||||
|
<button class="preset-btn" id="tscmMeetingSummaryBtn" onclick="tscmShowMeetingSummary()" style="width: 100%; padding: 8px; margin-top: 6px; display: none;">
|
||||||
|
View Meeting Summary
|
||||||
|
</button>
|
||||||
<div style="font-size: 9px; color: var(--text-muted); margin-top: 4px;">
|
<div style="font-size: 9px; color: var(--text-muted); margin-top: 4px;">
|
||||||
Devices detected during meetings get flagged
|
Devices detected during meetings get flagged
|
||||||
</div>
|
</div>
|
||||||
@@ -159,12 +162,18 @@
|
|||||||
<button class="preset-btn" onclick="tscmShowCapabilities()" style="font-size: 10px; padding: 8px;">
|
<button class="preset-btn" onclick="tscmShowCapabilities()" style="font-size: 10px; padding: 8px;">
|
||||||
Capabilities
|
Capabilities
|
||||||
</button>
|
</button>
|
||||||
|
<button class="preset-btn" onclick="tscmShowWifiIndicators()" style="font-size: 10px; padding: 8px;">
|
||||||
|
WiFi Indicators
|
||||||
|
</button>
|
||||||
<button class="preset-btn" onclick="tscmShowKnownDevices()" style="font-size: 10px; padding: 8px;">
|
<button class="preset-btn" onclick="tscmShowKnownDevices()" style="font-size: 10px; padding: 8px;">
|
||||||
Known Devices
|
Known Devices
|
||||||
</button>
|
</button>
|
||||||
<button class="preset-btn" onclick="tscmShowCases()" style="font-size: 10px; padding: 8px;">
|
<button class="preset-btn" onclick="tscmShowCases()" style="font-size: 10px; padding: 8px;">
|
||||||
Cases
|
Cases
|
||||||
</button>
|
</button>
|
||||||
|
<button class="preset-btn" onclick="tscmShowSchedules()" style="font-size: 10px; padding: 8px;">
|
||||||
|
Schedules
|
||||||
|
</button>
|
||||||
<button class="preset-btn" onclick="tscmShowPlaybooks()" style="font-size: 10px; padding: 8px;">
|
<button class="preset-btn" onclick="tscmShowPlaybooks()" style="font-size: 10px; padding: 8px;">
|
||||||
Playbooks
|
Playbooks
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1215,6 +1215,112 @@ def delete_known_device(identifier: str) -> bool:
|
|||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TSCM Schedule Functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def create_tscm_schedule(
|
||||||
|
name: str,
|
||||||
|
cron_expression: str,
|
||||||
|
sweep_type: str = 'standard',
|
||||||
|
baseline_id: int | None = None,
|
||||||
|
zone_name: str | None = None,
|
||||||
|
enabled: bool = True,
|
||||||
|
notify_on_threat: bool = True,
|
||||||
|
notify_email: str | None = None,
|
||||||
|
last_run: str | None = None,
|
||||||
|
next_run: str | None = None,
|
||||||
|
) -> int:
|
||||||
|
"""Create a new TSCM sweep schedule."""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.execute('''
|
||||||
|
INSERT INTO tscm_schedules
|
||||||
|
(name, baseline_id, zone_name, cron_expression, sweep_type,
|
||||||
|
enabled, last_run, next_run, notify_on_threat, notify_email)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
name,
|
||||||
|
baseline_id,
|
||||||
|
zone_name,
|
||||||
|
cron_expression,
|
||||||
|
sweep_type,
|
||||||
|
1 if enabled else 0,
|
||||||
|
last_run,
|
||||||
|
next_run,
|
||||||
|
1 if notify_on_threat else 0,
|
||||||
|
notify_email,
|
||||||
|
))
|
||||||
|
return cursor.lastrowid
|
||||||
|
|
||||||
|
|
||||||
|
def get_tscm_schedule(schedule_id: int) -> dict | None:
|
||||||
|
"""Get a TSCM schedule by ID."""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
'SELECT * FROM tscm_schedules WHERE id = ?',
|
||||||
|
(schedule_id,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_tscm_schedules(
|
||||||
|
enabled: bool | None = None,
|
||||||
|
limit: int = 200
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get all TSCM schedules."""
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if enabled is not None:
|
||||||
|
conditions.append('enabled = ?')
|
||||||
|
params.append(1 if enabled else 0)
|
||||||
|
|
||||||
|
where_clause = f'WHERE {" AND ".join(conditions)}' if conditions else ''
|
||||||
|
params.append(limit)
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.execute(f'''
|
||||||
|
SELECT * FROM tscm_schedules
|
||||||
|
{where_clause}
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?
|
||||||
|
''', params)
|
||||||
|
return [dict(row) for row in cursor]
|
||||||
|
|
||||||
|
|
||||||
|
def update_tscm_schedule(schedule_id: int, **fields) -> bool:
|
||||||
|
"""Update a TSCM schedule."""
|
||||||
|
if not fields:
|
||||||
|
return False
|
||||||
|
|
||||||
|
updates = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
for key, value in fields.items():
|
||||||
|
updates.append(f'{key} = ?')
|
||||||
|
params.append(value)
|
||||||
|
|
||||||
|
params.append(schedule_id)
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
f'UPDATE tscm_schedules SET {", ".join(updates)} WHERE id = ?',
|
||||||
|
params
|
||||||
|
)
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def delete_tscm_schedule(schedule_id: int) -> bool:
|
||||||
|
"""Delete a TSCM schedule."""
|
||||||
|
with get_db() as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
'DELETE FROM tscm_schedules WHERE id = ?',
|
||||||
|
(schedule_id,)
|
||||||
|
)
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
def is_known_good_device(identifier: str, location: str | None = None) -> dict | None:
|
def is_known_good_device(identifier: str, location: str | None = None) -> dict | None:
|
||||||
"""Check if a device is in the known-good registry for a location."""
|
"""Check if a device is in the known-good registry for a location."""
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
|||||||
+8
-3
@@ -246,6 +246,7 @@ class SSTVDecoder:
|
|||||||
self._doppler_tracker = DopplerTracker('ISS')
|
self._doppler_tracker = DopplerTracker('ISS')
|
||||||
self._doppler_enabled = False
|
self._doppler_enabled = False
|
||||||
self._last_doppler_info: DopplerInfo | None = None
|
self._last_doppler_info: DopplerInfo | None = None
|
||||||
|
self._file_decoder: str | None = None
|
||||||
|
|
||||||
# Ensure output directory exists
|
# Ensure output directory exists
|
||||||
self._output_dir.mkdir(parents=True, exist_ok=True)
|
self._output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -268,6 +269,7 @@ class SSTVDecoder:
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(['which', 'slowrx'], capture_output=True, timeout=5)
|
result = subprocess.run(['which', 'slowrx'], capture_output=True, timeout=5)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
|
self._file_decoder = 'slowrx'
|
||||||
return 'slowrx'
|
return 'slowrx'
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -277,7 +279,8 @@ class SSTVDecoder:
|
|||||||
# Check for Python sstv package
|
# Check for Python sstv package
|
||||||
try:
|
try:
|
||||||
import sstv
|
import sstv
|
||||||
return 'python-sstv'
|
self._file_decoder = 'python-sstv'
|
||||||
|
return None
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -693,7 +696,9 @@ class SSTVDecoder:
|
|||||||
|
|
||||||
images = []
|
images = []
|
||||||
|
|
||||||
if self._decoder == 'slowrx':
|
decoder = self._decoder or self._file_decoder
|
||||||
|
|
||||||
|
if decoder == 'slowrx':
|
||||||
# Use slowrx with file input
|
# Use slowrx with file input
|
||||||
output_file = self._output_dir / f"sstv_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
output_file = self._output_dir / f"sstv_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
|
||||||
|
|
||||||
@@ -715,7 +720,7 @@ class SSTVDecoder:
|
|||||||
)
|
)
|
||||||
images.append(image)
|
images.append(image)
|
||||||
|
|
||||||
elif self._decoder == 'python-sstv':
|
elif decoder == 'python-sstv':
|
||||||
# Use Python sstv library
|
# Use Python sstv library
|
||||||
try:
|
try:
|
||||||
from sstv.decode import SSTVDecoder as PythonSSTVDecoder
|
from sstv.decode import SSTVDecoder as PythonSSTVDecoder
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ class DeviceProfile:
|
|||||||
# Output
|
# Output
|
||||||
confidence: float = 0.0
|
confidence: float = 0.0
|
||||||
recommended_action: str = 'monitor'
|
recommended_action: str = 'monitor'
|
||||||
|
known_device: bool = False
|
||||||
|
known_device_name: Optional[str] = None
|
||||||
|
score_modifier: int = 0
|
||||||
|
|
||||||
def add_rssi_sample(self, rssi: int) -> None:
|
def add_rssi_sample(self, rssi: int) -> None:
|
||||||
"""Add an RSSI sample with timestamp."""
|
"""Add an RSSI sample with timestamp."""
|
||||||
@@ -208,6 +211,26 @@ class DeviceProfile:
|
|||||||
indicator_count = len(self.indicators)
|
indicator_count = len(self.indicators)
|
||||||
self.confidence = min(1.0, (indicator_count * 0.15) + (self.total_score * 0.05))
|
self.confidence = min(1.0, (indicator_count * 0.15) + (self.total_score * 0.05))
|
||||||
|
|
||||||
|
def apply_score_modifier(self, modifier: int | None) -> None:
|
||||||
|
"""Apply a score modifier (e.g., known-good device adjustment)."""
|
||||||
|
base_score = sum(i.score for i in self.indicators)
|
||||||
|
modifier_val = int(modifier) if modifier is not None else 0
|
||||||
|
self.score_modifier = modifier_val
|
||||||
|
self.total_score = max(0, base_score + modifier_val)
|
||||||
|
|
||||||
|
if self.total_score >= 6:
|
||||||
|
self.risk_level = RiskLevel.HIGH_INTEREST
|
||||||
|
self.recommended_action = 'investigate'
|
||||||
|
elif self.total_score >= 3:
|
||||||
|
self.risk_level = RiskLevel.NEEDS_REVIEW
|
||||||
|
self.recommended_action = 'review'
|
||||||
|
else:
|
||||||
|
self.risk_level = RiskLevel.INFORMATIONAL
|
||||||
|
self.recommended_action = 'monitor'
|
||||||
|
|
||||||
|
indicator_count = len(self.indicators)
|
||||||
|
self.confidence = min(1.0, (indicator_count * 0.15) + (self.total_score * 0.05))
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert to dictionary for JSON serialization."""
|
"""Convert to dictionary for JSON serialization."""
|
||||||
return {
|
return {
|
||||||
@@ -232,10 +255,13 @@ class DeviceProfile:
|
|||||||
for i in self.indicators
|
for i in self.indicators
|
||||||
],
|
],
|
||||||
'total_score': self.total_score,
|
'total_score': self.total_score,
|
||||||
|
'score_modifier': self.score_modifier,
|
||||||
'risk_level': self.risk_level.value,
|
'risk_level': self.risk_level.value,
|
||||||
'confidence': round(self.confidence, 2),
|
'confidence': round(self.confidence, 2),
|
||||||
'recommended_action': self.recommended_action,
|
'recommended_action': self.recommended_action,
|
||||||
'correlated_devices': self.correlated_devices,
|
'correlated_devices': self.correlated_devices,
|
||||||
|
'known_device': self.known_device,
|
||||||
|
'known_device_name': self.known_device_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -286,6 +312,7 @@ class CorrelationEngine:
|
|||||||
self.device_profiles: dict[str, DeviceProfile] = {}
|
self.device_profiles: dict[str, DeviceProfile] = {}
|
||||||
self.meeting_windows: list[tuple[datetime, datetime]] = []
|
self.meeting_windows: list[tuple[datetime, datetime]] = []
|
||||||
self.correlation_window = timedelta(minutes=5)
|
self.correlation_window = timedelta(minutes=5)
|
||||||
|
self._known_device_cache: dict[str, dict | None] = {}
|
||||||
|
|
||||||
def start_meeting_window(self) -> None:
|
def start_meeting_window(self) -> None:
|
||||||
"""Mark the start of a sensitive period (meeting)."""
|
"""Mark the start of a sensitive period (meeting)."""
|
||||||
@@ -310,6 +337,54 @@ class CorrelationEngine:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _lookup_known_device(self, identifier: str, protocol: str) -> dict | None:
|
||||||
|
"""Lookup known-good device details with light normalization."""
|
||||||
|
cache_key = f"{protocol}:{identifier}"
|
||||||
|
if cache_key in self._known_device_cache:
|
||||||
|
return self._known_device_cache[cache_key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils.database import is_known_good_device
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
if identifier:
|
||||||
|
candidates.append(str(identifier))
|
||||||
|
|
||||||
|
if protocol == 'rf':
|
||||||
|
try:
|
||||||
|
freq_val = float(identifier)
|
||||||
|
candidates.append(f"{freq_val:.3f}")
|
||||||
|
candidates.append(f"{freq_val:.1f}")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
known = None
|
||||||
|
for cand in candidates:
|
||||||
|
if not cand:
|
||||||
|
continue
|
||||||
|
known = is_known_good_device(str(cand).upper())
|
||||||
|
if known:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
known = None
|
||||||
|
|
||||||
|
self._known_device_cache[cache_key] = known
|
||||||
|
return known
|
||||||
|
|
||||||
|
def _apply_known_device_modifier(self, profile: DeviceProfile, identifier: str, protocol: str) -> None:
|
||||||
|
"""Apply known-good score modifier and update profile metadata."""
|
||||||
|
known = self._lookup_known_device(identifier, protocol)
|
||||||
|
if known:
|
||||||
|
profile.known_device = True
|
||||||
|
profile.known_device_name = known.get('name') if isinstance(known, dict) else None
|
||||||
|
modifier = known.get('score_modifier', 0) if isinstance(known, dict) else 0
|
||||||
|
else:
|
||||||
|
profile.known_device = False
|
||||||
|
profile.known_device_name = None
|
||||||
|
modifier = 0
|
||||||
|
|
||||||
|
profile.apply_score_modifier(modifier)
|
||||||
|
|
||||||
def get_or_create_profile(self, identifier: str, protocol: str) -> DeviceProfile:
|
def get_or_create_profile(self, identifier: str, protocol: str) -> DeviceProfile:
|
||||||
"""Get existing profile or create new one."""
|
"""Get existing profile or create new one."""
|
||||||
key = f"{protocol}:{identifier}"
|
key = f"{protocol}:{identifier}"
|
||||||
@@ -583,6 +658,8 @@ class CorrelationEngine:
|
|||||||
)
|
)
|
||||||
profile.device_type = 'Samsung SmartTag'
|
profile.device_type = 'Samsung SmartTag'
|
||||||
|
|
||||||
|
self._apply_known_device_modifier(profile, mac, 'bluetooth')
|
||||||
|
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
def analyze_wifi_device(self, device: dict) -> DeviceProfile:
|
def analyze_wifi_device(self, device: dict) -> DeviceProfile:
|
||||||
@@ -695,6 +772,8 @@ class CorrelationEngine:
|
|||||||
{'rssi': latest_rssi}
|
{'rssi': latest_rssi}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._apply_known_device_modifier(profile, bssid, 'wifi')
|
||||||
|
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
def analyze_rf_signal(self, signal: dict) -> DeviceProfile:
|
def analyze_rf_signal(self, signal: dict) -> DeviceProfile:
|
||||||
@@ -785,6 +864,8 @@ class CorrelationEngine:
|
|||||||
{'during_meeting': True}
|
{'during_meeting': True}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._apply_known_device_modifier(profile, freq_key, 'rf')
|
||||||
|
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
def correlate_devices(self) -> list[dict]:
|
def correlate_devices(self) -> list[dict]:
|
||||||
@@ -887,6 +968,10 @@ class CorrelationEngine:
|
|||||||
}
|
}
|
||||||
correlations.append(correlation)
|
correlations.append(correlation)
|
||||||
|
|
||||||
|
# Re-apply known-good modifiers after correlation boosts
|
||||||
|
for profile in self.device_profiles.values():
|
||||||
|
self._apply_known_device_modifier(profile, profile.identifier, profile.protocol)
|
||||||
|
|
||||||
return correlations
|
return correlations
|
||||||
|
|
||||||
def get_high_interest_devices(self) -> list[DeviceProfile]:
|
def get_high_interest_devices(self) -> list[DeviceProfile]:
|
||||||
|
|||||||
Reference in New Issue
Block a user