mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
First GSM SPY addition
This commit is contained in:
44
app.py
44
app.py
@@ -39,6 +39,7 @@ from utils.constants import (
|
|||||||
MAX_VESSEL_AGE_SECONDS,
|
MAX_VESSEL_AGE_SECONDS,
|
||||||
MAX_DSC_MESSAGE_AGE_SECONDS,
|
MAX_DSC_MESSAGE_AGE_SECONDS,
|
||||||
MAX_DEAUTH_ALERTS_AGE_SECONDS,
|
MAX_DEAUTH_ALERTS_AGE_SECONDS,
|
||||||
|
MAX_GSM_AGE_SECONDS,
|
||||||
QUEUE_MAX_SIZE,
|
QUEUE_MAX_SIZE,
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
@@ -181,6 +182,15 @@ deauth_detector = None
|
|||||||
deauth_detector_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
deauth_detector_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
deauth_detector_lock = threading.Lock()
|
deauth_detector_lock = threading.Lock()
|
||||||
|
|
||||||
|
# GSM Spy
|
||||||
|
gsm_spy_process = None
|
||||||
|
gsm_spy_monitor_process = None # For grgsm_livemon when monitoring specific tower
|
||||||
|
gsm_spy_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
|
||||||
|
gsm_spy_lock = threading.Lock()
|
||||||
|
gsm_spy_active_device = None
|
||||||
|
gsm_spy_selected_arfcn = None
|
||||||
|
gsm_spy_region = 'Americas' # Default band
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# GLOBAL STATE DICTIONARIES
|
# GLOBAL STATE DICTIONARIES
|
||||||
# ============================================
|
# ============================================
|
||||||
@@ -213,6 +223,16 @@ dsc_messages = DataStore(max_age_seconds=MAX_DSC_MESSAGE_AGE_SECONDS, name='dsc_
|
|||||||
# Deauth alerts - using DataStore for automatic cleanup
|
# Deauth alerts - using DataStore for automatic cleanup
|
||||||
deauth_alerts = DataStore(max_age_seconds=MAX_DEAUTH_ALERTS_AGE_SECONDS, name='deauth_alerts')
|
deauth_alerts = DataStore(max_age_seconds=MAX_DEAUTH_ALERTS_AGE_SECONDS, name='deauth_alerts')
|
||||||
|
|
||||||
|
# GSM Spy data stores
|
||||||
|
gsm_spy_towers = DataStore(
|
||||||
|
max_age_seconds=MAX_GSM_AGE_SECONDS,
|
||||||
|
name='gsm_spy_towers'
|
||||||
|
)
|
||||||
|
gsm_spy_devices = DataStore(
|
||||||
|
max_age_seconds=MAX_GSM_AGE_SECONDS,
|
||||||
|
name='gsm_spy_devices'
|
||||||
|
)
|
||||||
|
|
||||||
# Satellite state
|
# Satellite state
|
||||||
satellite_passes = [] # Predicted satellite passes (not auto-cleaned, calculated)
|
satellite_passes = [] # Predicted satellite passes (not auto-cleaned, calculated)
|
||||||
|
|
||||||
@@ -225,6 +245,8 @@ cleanup_manager.register(adsb_aircraft)
|
|||||||
cleanup_manager.register(ais_vessels)
|
cleanup_manager.register(ais_vessels)
|
||||||
cleanup_manager.register(dsc_messages)
|
cleanup_manager.register(dsc_messages)
|
||||||
cleanup_manager.register(deauth_alerts)
|
cleanup_manager.register(deauth_alerts)
|
||||||
|
cleanup_manager.register(gsm_spy_towers)
|
||||||
|
cleanup_manager.register(gsm_spy_devices)
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# SDR DEVICE REGISTRY
|
# SDR DEVICE REGISTRY
|
||||||
@@ -652,6 +674,7 @@ def kill_all() -> Response:
|
|||||||
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
||||||
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
||||||
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
||||||
|
global gsm_spy_process, gsm_spy_monitor_process
|
||||||
|
|
||||||
# Import adsb and ais modules to reset their state
|
# Import adsb and ais modules to reset their state
|
||||||
from routes import adsb as adsb_module
|
from routes import adsb as adsb_module
|
||||||
@@ -663,7 +686,8 @@ def kill_all() -> Response:
|
|||||||
'rtl_fm', 'multimon-ng', 'rtl_433',
|
'rtl_fm', 'multimon-ng', 'rtl_433',
|
||||||
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
'airodump-ng', 'aireplay-ng', 'airmon-ng',
|
||||||
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher',
|
'dump1090', 'acarsdec', 'direwolf', 'AIS-catcher',
|
||||||
'hcitool', 'bluetoothctl'
|
'hcitool', 'bluetoothctl', 'grgsm_scanner', 'grgsm_livemon',
|
||||||
|
'tshark'
|
||||||
]
|
]
|
||||||
|
|
||||||
for proc in processes_to_kill:
|
for proc in processes_to_kill:
|
||||||
@@ -727,6 +751,24 @@ def kill_all() -> Response:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Reset GSM Spy state
|
||||||
|
with gsm_spy_lock:
|
||||||
|
if gsm_spy_process:
|
||||||
|
try:
|
||||||
|
safe_terminate(gsm_spy_process, 'grgsm_scanner')
|
||||||
|
killed.append('grgsm_scanner')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
gsm_spy_process = None
|
||||||
|
|
||||||
|
if gsm_spy_monitor_process:
|
||||||
|
try:
|
||||||
|
safe_terminate(gsm_spy_monitor_process, 'grgsm_livemon')
|
||||||
|
killed.append('grgsm_livemon')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
gsm_spy_monitor_process = None
|
||||||
|
|
||||||
# Clear SDR device registry
|
# Clear SDR device registry
|
||||||
with sdr_device_registry_lock:
|
with sdr_device_registry_lock:
|
||||||
sdr_device_registry.clear()
|
sdr_device_registry.clear()
|
||||||
|
|||||||
@@ -200,6 +200,14 @@ UPDATE_CHECK_INTERVAL_HOURS = _get_env_int('UPDATE_CHECK_INTERVAL_HOURS', 6)
|
|||||||
ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin')
|
ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin')
|
||||||
ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', 'admin')
|
ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', 'admin')
|
||||||
|
|
||||||
|
# GSM Spy settings
|
||||||
|
GSM_OPENCELLID_API_KEY = _get_env('GSM_OPENCELLID_API_KEY', 'pk.68c92ecb85886de7b50ed5a4c73f9504')
|
||||||
|
GSM_OPENCELLID_API_URL = _get_env('GSM_OPENCELLID_API_URL', 'https://opencellid.org/cell/get')
|
||||||
|
GSM_API_DAILY_LIMIT = _get_env_int('GSM_API_DAILY_LIMIT', 1000)
|
||||||
|
GSM_TA_METERS_PER_UNIT = _get_env_int('GSM_TA_METERS_PER_UNIT', 554)
|
||||||
|
GSM_UPDATE_INTERVAL = _get_env_float('GSM_UPDATE_INTERVAL', 2.0)
|
||||||
|
GSM_MAX_AGE_SECONDS = _get_env_int('GSM_MAX_AGE_SECONDS', 300)
|
||||||
|
|
||||||
def configure_logging() -> None:
|
def configure_logging() -> None:
|
||||||
"""Configure application logging."""
|
"""Configure application logging."""
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ def register_blueprints(app):
|
|||||||
from .offline import offline_bp
|
from .offline import offline_bp
|
||||||
from .updater import updater_bp
|
from .updater import updater_bp
|
||||||
from .sstv import sstv_bp
|
from .sstv import sstv_bp
|
||||||
|
from .gsm_spy import gsm_spy_bp
|
||||||
|
|
||||||
app.register_blueprint(pager_bp)
|
app.register_blueprint(pager_bp)
|
||||||
app.register_blueprint(sensor_bp)
|
app.register_blueprint(sensor_bp)
|
||||||
@@ -51,6 +52,7 @@ def register_blueprints(app):
|
|||||||
app.register_blueprint(offline_bp) # Offline mode settings
|
app.register_blueprint(offline_bp) # Offline mode settings
|
||||||
app.register_blueprint(updater_bp) # GitHub update checking
|
app.register_blueprint(updater_bp) # GitHub update checking
|
||||||
app.register_blueprint(sstv_bp) # ISS SSTV decoder
|
app.register_blueprint(sstv_bp) # ISS SSTV decoder
|
||||||
|
app.register_blueprint(gsm_spy_bp) # GSM cellular intelligence
|
||||||
|
|
||||||
# Initialize TSCM state with queue and lock from app
|
# Initialize TSCM state with queue and lock from app
|
||||||
import app as app_module
|
import app as app_module
|
||||||
|
|||||||
1171
routes/gsm_spy.py
Normal file
1171
routes/gsm_spy.py
Normal file
File diff suppressed because it is too large
Load Diff
127
setup.sh
127
setup.sh
@@ -533,6 +533,52 @@ install_macos_packages() {
|
|||||||
progress "Installing gpsd"
|
progress "Installing gpsd"
|
||||||
brew_install gpsd
|
brew_install gpsd
|
||||||
|
|
||||||
|
# gr-gsm for GSM Intelligence
|
||||||
|
if ! cmd_exists grgsm_scanner; then
|
||||||
|
echo
|
||||||
|
info "gr-gsm provides GSM cellular signal decoding..."
|
||||||
|
if ask_yes_no "Do you want to install gr-gsm?"; then
|
||||||
|
progress "Installing gr-gsm"
|
||||||
|
brew_install gnuradio
|
||||||
|
(brew_install gr-gsm) || {
|
||||||
|
warn "gr-gsm not available in Homebrew, attempting manual build..."
|
||||||
|
# Manual build instructions
|
||||||
|
if ask_yes_no "Attempt to build gr-gsm from source? (requires CMake and build tools)"; then
|
||||||
|
info "Cloning gr-gsm repository..."
|
||||||
|
git clone https://github.com/ptrkrysik/gr-gsm.git /tmp/gr-gsm
|
||||||
|
cd /tmp/gr-gsm
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake ..
|
||||||
|
make -j$(sysctl -n hw.ncpu)
|
||||||
|
sudo make install
|
||||||
|
cd ~
|
||||||
|
rm -rf /tmp/gr-gsm
|
||||||
|
ok "gr-gsm installed successfully"
|
||||||
|
else
|
||||||
|
warn "Skipping gr-gsm source build. GSM Spy feature will not work."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
else
|
||||||
|
warn "Skipping gr-gsm installation. GSM Spy feature will not work."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "gr-gsm already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wireshark (tshark) for packet analysis
|
||||||
|
if ! cmd_exists tshark; then
|
||||||
|
echo
|
||||||
|
info "tshark is used for GSM packet parsing..."
|
||||||
|
if ask_yes_no "Do you want to install tshark?"; then
|
||||||
|
progress "Installing Wireshark (tshark)"
|
||||||
|
brew_install wireshark
|
||||||
|
else
|
||||||
|
warn "Skipping tshark installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "tshark already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Installing Ubertooth tools (optional)"
|
progress "Installing Ubertooth tools (optional)"
|
||||||
if ! cmd_exists ubertooth-btle; then
|
if ! cmd_exists ubertooth-btle; then
|
||||||
echo
|
echo
|
||||||
@@ -961,6 +1007,87 @@ install_debian_packages() {
|
|||||||
progress "Installing gpsd"
|
progress "Installing gpsd"
|
||||||
apt_install gpsd gpsd-clients || true
|
apt_install gpsd gpsd-clients || true
|
||||||
|
|
||||||
|
# gr-gsm for GSM Intelligence
|
||||||
|
if ! cmd_exists grgsm_scanner; then
|
||||||
|
echo
|
||||||
|
info "gr-gsm provides GSM cellular signal decoding..."
|
||||||
|
if ask_yes_no "Do you want to install gr-gsm?"; then
|
||||||
|
progress "Installing GNU Radio and gr-gsm"
|
||||||
|
# Try to install gr-gsm directly from package repositories
|
||||||
|
apt_install gnuradio gnuradio-dev gr-osmosdr gr-gsm || {
|
||||||
|
warn "gr-gsm package not available in repositories. Attempting source build..."
|
||||||
|
|
||||||
|
# Fallback: Build from source
|
||||||
|
progress "Building gr-gsm from source"
|
||||||
|
apt_install git cmake libboost-all-dev libcppunit-dev swig \
|
||||||
|
doxygen liblog4cpp5-dev python3-scipy python3-numpy \
|
||||||
|
libvolk-dev libuhd-dev libfftw3-dev || true
|
||||||
|
|
||||||
|
info "Cloning gr-gsm repository..."
|
||||||
|
if [ -d /tmp/gr-gsm ]; then
|
||||||
|
rm -rf /tmp/gr-gsm
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone https://github.com/ptrkrysik/gr-gsm.git /tmp/gr-gsm || {
|
||||||
|
warn "Failed to clone gr-gsm repository. GSM Spy will not be available."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cd /tmp/gr-gsm
|
||||||
|
mkdir -p build && cd build
|
||||||
|
|
||||||
|
# Try to find GNU Radio cmake files
|
||||||
|
if [ -d /usr/lib/x86_64-linux-gnu/cmake/gnuradio ]; then
|
||||||
|
export CMAKE_PREFIX_PATH="/usr/lib/x86_64-linux-gnu/cmake/gnuradio:$CMAKE_PREFIX_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Running CMake configuration..."
|
||||||
|
if cmake .. 2>/dev/null; then
|
||||||
|
info "Compiling gr-gsm (this may take several minutes)..."
|
||||||
|
if make -j$(nproc) 2>/dev/null; then
|
||||||
|
$SUDO make install
|
||||||
|
$SUDO ldconfig
|
||||||
|
cd ~
|
||||||
|
rm -rf /tmp/gr-gsm
|
||||||
|
ok "gr-gsm built and installed successfully"
|
||||||
|
else
|
||||||
|
warn "gr-gsm compilation failed. GSM Spy feature will not work."
|
||||||
|
cd ~
|
||||||
|
rm -rf /tmp/gr-gsm
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "gr-gsm CMake configuration failed. GNU Radio 3.8+ may not be available."
|
||||||
|
cd ~
|
||||||
|
rm -rf /tmp/gr-gsm
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
if cmd_exists grgsm_scanner; then
|
||||||
|
ok "gr-gsm installed successfully"
|
||||||
|
else
|
||||||
|
warn "gr-gsm installation incomplete. GSM Spy feature will not work."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Skipping gr-gsm installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "gr-gsm already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wireshark (tshark)
|
||||||
|
if ! cmd_exists tshark; then
|
||||||
|
echo
|
||||||
|
info "Installing tshark for GSM packet analysis..."
|
||||||
|
apt_install tshark || true
|
||||||
|
# Allow non-root capture
|
||||||
|
$SUDO dpkg-reconfigure wireshark-common 2>/dev/null || true
|
||||||
|
$SUDO usermod -a -G wireshark $USER 2>/dev/null || true
|
||||||
|
ok "tshark installed. You may need to re-login for wireshark group permissions."
|
||||||
|
else
|
||||||
|
ok "tshark already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
progress "Installing Python packages"
|
progress "Installing Python packages"
|
||||||
apt_install python3-venv python3-pip || true
|
apt_install python3-venv python3-pip || true
|
||||||
# Install Python packages via apt (more reliable than pip on modern Debian/Ubuntu)
|
# Install Python packages via apt (more reliable than pip on modern Debian/Ubuntu)
|
||||||
|
|||||||
622
static/css/gsm_spy_dashboard.css
Normal file
622
static/css/gsm_spy_dashboard.css
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
/* GSM SPY Dashboard Styles */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-mono: 'IBM Plex Mono', 'JetBrains Mono', 'Courier New', monospace;
|
||||||
|
--bg-dark: #0b1118;
|
||||||
|
--bg-panel: #101823;
|
||||||
|
--bg-panel-hover: #1a2331;
|
||||||
|
--border-color: #263246;
|
||||||
|
--accent-green: #38c180;
|
||||||
|
--accent-cyan: #4aa3ff;
|
||||||
|
--accent-red: #e25d5d;
|
||||||
|
--accent-yellow: #ffa500;
|
||||||
|
--text-primary: #e8e8e8;
|
||||||
|
--text-secondary: #888;
|
||||||
|
--text-dim: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
background: var(--bg-dark);
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Radar background and scanline */
|
||||||
|
.radar-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanline {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
opacity: 0.3;
|
||||||
|
animation: scan 3s linear infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
from { transform: translateY(0); }
|
||||||
|
to { transform: translateY(100vh); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 60px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.active {
|
||||||
|
background: var(--accent-green);
|
||||||
|
animation: pulse-dot 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.error {
|
||||||
|
background: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats strip */
|
||||||
|
.stats-strip {
|
||||||
|
position: fixed;
|
||||||
|
top: 60px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 50px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 0 20px;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-green);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard layout */
|
||||||
|
.dashboard {
|
||||||
|
position: fixed;
|
||||||
|
top: 110px;
|
||||||
|
bottom: 80px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr 300px;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar panels */
|
||||||
|
.left-sidebar, .right-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signal source panel */
|
||||||
|
.signal-source select,
|
||||||
|
.region-selector select {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-selector {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-selector label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-info {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected tower info */
|
||||||
|
.selected-info {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info.empty {
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info > div {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-info strong {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tower and device lists */
|
||||||
|
.tower-list, .device-list, .alert-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-item, .device-item, .alert-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-item:hover, .device-item:hover {
|
||||||
|
background: var(--bg-panel-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-item:last-child, .device-item:last-child, .alert-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-item.rogue {
|
||||||
|
border-left: 3px solid var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-cid {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-signal {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-operator {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-item-id {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-green);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-ta {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-item {
|
||||||
|
background: rgba(226, 93, 93, 0.1);
|
||||||
|
border-left: 3px solid var(--accent-red);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-item strong {
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-item small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map container */
|
||||||
|
.map-container {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gsmMap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map markers */
|
||||||
|
.tower-marker {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-green);
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 0 8px rgba(56, 195, 128, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tower-marker.rogue {
|
||||||
|
background: var(--accent-red);
|
||||||
|
box-shadow: 0 0 8px rgba(226, 93, 93, 0.8);
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 50% { opacity: 1; }
|
||||||
|
51%, 100% { opacity: 0.3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-blip {
|
||||||
|
animation: pulse-blip 5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-blip {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls bar */
|
||||||
|
.controls-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 80px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group-items {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input fields */
|
||||||
|
input[type="text"], input[type="number"], select {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus, input[type="number"]:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button {
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.active {
|
||||||
|
background: var(--accent-red);
|
||||||
|
animation: pulse-btn 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-btn {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(226, 93, 93, 0.7); }
|
||||||
|
50% { box-shadow: 0 0 0 10px rgba(226, 93, 93, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: var(--text-dim);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GPS indicator */
|
||||||
|
.gps-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gps-indicator::before {
|
||||||
|
content: '';
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gps-indicator.active::before {
|
||||||
|
background: var(--accent-green);
|
||||||
|
animation: pulse-dot 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
padding: 30px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.dashboard {
|
||||||
|
grid-template-columns: 250px 1fr 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.dashboard {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar, .right-sidebar {
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility classes */
|
||||||
|
.text-success { color: var(--accent-green); }
|
||||||
|
.text-danger { color: var(--accent-red); }
|
||||||
|
.text-warning { color: var(--accent-yellow); }
|
||||||
|
.text-info { color: var(--accent-cyan); }
|
||||||
|
.text-muted { color: var(--text-secondary); }
|
||||||
|
|
||||||
|
.mt-1 { margin-top: 8px; }
|
||||||
|
.mt-2 { margin-top: 16px; }
|
||||||
|
.mb-1 { margin-bottom: 8px; }
|
||||||
|
.mb-2 { margin-bottom: 16px; }
|
||||||
|
|
||||||
|
/* Advanced Analysis Results Panel */
|
||||||
|
.analysis-results {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-content {
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-stat {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-stat:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-stat-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-stat-value {
|
||||||
|
color: var(--accent-green);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-device-item {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 6px 0;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
border-radius: 3px;
|
||||||
|
border-left: 3px solid var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-warning {
|
||||||
|
color: var(--accent-yellow);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 165, 0, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
2194
templates/gsm_spy_dashboard.html
Normal file
2194
templates/gsm_spy_dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -167,6 +167,10 @@
|
|||||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/></svg></span>
|
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/></svg></span>
|
||||||
<span class="mode-name">Vessels</span>
|
<span class="mode-name">Vessels</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/gsm_spy/dashboard" class="mode-card mode-card-sm" style="text-decoration: none;">
|
||||||
|
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/><path d="M8 6h8M8 10h8M8 14h8"/></svg></span>
|
||||||
|
<span class="mode-name">GSM SPY</span>
|
||||||
|
</a>
|
||||||
<button class="mode-card mode-card-sm" onclick="selectMode('aprs')">
|
<button class="mode-card mode-card-sm" onclick="selectMode('aprs')">
|
||||||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span>
|
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span>
|
||||||
<span class="mode-name">APRS</span>
|
<span class="mode-name">APRS</span>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
{{ mode_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||||
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
{{ mode_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg>', '/adsb/dashboard') }}
|
||||||
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
{{ mode_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/><path d="M12 6l4 3"/></svg>', '/ais/dashboard') }}
|
||||||
|
{{ mode_item('gsm', 'GSM SPY', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/><path d="M8 6h8M8 10h8M8 14h8"/></svg>', '/gsm_spy/dashboard') }}
|
||||||
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
{{ mode_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>') }}
|
||||||
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
{{ mode_item('listening', 'Listening Post', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||||
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
{{ mode_item('spystations', 'Spy Stations', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>') }}
|
||||||
|
|||||||
@@ -274,3 +274,14 @@ MAX_DEAUTH_ALERTS_AGE_SECONDS = 300 # 5 minutes
|
|||||||
|
|
||||||
# Deauth detector sniff timeout (seconds)
|
# Deauth detector sniff timeout (seconds)
|
||||||
DEAUTH_SNIFF_TIMEOUT = 0.5
|
DEAUTH_SNIFF_TIMEOUT = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GSM SPY (Cellular Intelligence)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Maximum age for GSM tower/device data in DataStore (seconds)
|
||||||
|
MAX_GSM_AGE_SECONDS = 300 # 5 minutes
|
||||||
|
|
||||||
|
# Timing Advance conversion to meters
|
||||||
|
GSM_TA_METERS_PER_UNIT = 554
|
||||||
|
|||||||
@@ -352,6 +352,134 @@ def init_db() -> None:
|
|||||||
ON tscm_cases(status, created_at)
|
ON tscm_cases(status, created_at)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# GSM (Global System for Mobile) Intelligence Tables
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
# gsm_cells - Known cell towers (OpenCellID cache)
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS gsm_cells (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mcc INTEGER NOT NULL,
|
||||||
|
mnc INTEGER NOT NULL,
|
||||||
|
lac INTEGER NOT NULL,
|
||||||
|
cid INTEGER NOT NULL,
|
||||||
|
lat REAL,
|
||||||
|
lon REAL,
|
||||||
|
azimuth INTEGER,
|
||||||
|
range_meters INTEGER,
|
||||||
|
samples INTEGER,
|
||||||
|
radio TEXT,
|
||||||
|
operator TEXT,
|
||||||
|
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_verified TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
metadata TEXT,
|
||||||
|
UNIQUE(mcc, mnc, lac, cid)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# gsm_rogues - Detected rogue towers / IMSI catchers
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS gsm_rogues (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
arfcn INTEGER NOT NULL,
|
||||||
|
mcc INTEGER,
|
||||||
|
mnc INTEGER,
|
||||||
|
lac INTEGER,
|
||||||
|
cid INTEGER,
|
||||||
|
signal_strength REAL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
threat_level TEXT DEFAULT 'medium',
|
||||||
|
detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
location_lat REAL,
|
||||||
|
location_lon REAL,
|
||||||
|
acknowledged BOOLEAN DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
metadata TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# gsm_signals - 60-day archive of signal observations
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS gsm_signals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
imsi TEXT,
|
||||||
|
tmsi TEXT,
|
||||||
|
mcc INTEGER,
|
||||||
|
mnc INTEGER,
|
||||||
|
lac INTEGER,
|
||||||
|
cid INTEGER,
|
||||||
|
ta_value INTEGER,
|
||||||
|
signal_strength REAL,
|
||||||
|
arfcn INTEGER,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
metadata TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# gsm_tmsi_log - 24-hour raw pings for crowd density
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS gsm_tmsi_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
tmsi TEXT NOT NULL,
|
||||||
|
lac INTEGER,
|
||||||
|
cid INTEGER,
|
||||||
|
ta_value INTEGER,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# gsm_velocity_log - 1-hour buffer for movement tracking
|
||||||
|
conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS gsm_velocity_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
device_id TEXT NOT NULL,
|
||||||
|
prev_ta INTEGER,
|
||||||
|
curr_ta INTEGER,
|
||||||
|
prev_cid INTEGER,
|
||||||
|
curr_cid INTEGER,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
estimated_velocity REAL,
|
||||||
|
metadata TEXT
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# GSM indexes for performance
|
||||||
|
conn.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gsm_cells_location
|
||||||
|
ON gsm_cells(lat, lon)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gsm_cells_identity
|
||||||
|
ON gsm_cells(mcc, mnc, lac, cid)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gsm_rogues_severity
|
||||||
|
ON gsm_rogues(threat_level, detected_at)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gsm_signals_cell_time
|
||||||
|
ON gsm_signals(cid, lac, timestamp)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gsm_signals_device
|
||||||
|
ON gsm_signals(imsi, tmsi, timestamp)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gsm_tmsi_log_time
|
||||||
|
ON gsm_tmsi_log(timestamp)
|
||||||
|
''')
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_gsm_velocity_log_device
|
||||||
|
ON gsm_velocity_log(device_id, timestamp)
|
||||||
|
''')
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# DSC (Digital Selective Calling) Tables
|
# DSC (Digital Selective Calling) Tables
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
|
|||||||
@@ -443,6 +443,38 @@ TOOL_DEPENDENCIES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'gsm': {
|
||||||
|
'name': 'GSM Intelligence',
|
||||||
|
'tools': {
|
||||||
|
'grgsm_scanner': {
|
||||||
|
'required': True,
|
||||||
|
'description': 'gr-gsm scanner for finding GSM towers',
|
||||||
|
'install': {
|
||||||
|
'apt': 'Build gr-gsm from source: https://github.com/ptrkrysik/gr-gsm',
|
||||||
|
'brew': 'brew install gr-gsm (may require manual build)',
|
||||||
|
'manual': 'https://github.com/ptrkrysik/gr-gsm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'grgsm_livemon': {
|
||||||
|
'required': True,
|
||||||
|
'description': 'gr-gsm live monitor for decoding GSM signals',
|
||||||
|
'install': {
|
||||||
|
'apt': 'Included with gr-gsm package',
|
||||||
|
'brew': 'Included with gr-gsm',
|
||||||
|
'manual': 'Included with gr-gsm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'tshark': {
|
||||||
|
'required': True,
|
||||||
|
'description': 'Wireshark CLI for parsing GSM packets',
|
||||||
|
'install': {
|
||||||
|
'apt': 'sudo apt-get install tshark',
|
||||||
|
'brew': 'brew install wireshark',
|
||||||
|
'manual': 'https://www.wireshark.org/download.html'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user