Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
847750f
Add LAN SPY network scanning mode
JonanOribe Feb 7, 2026
e25f13f
Documentation & installation method updated
JonanOribe Feb 7, 2026
575d0e5
navigation update
JonanOribe Feb 7, 2026
1ee64a6
Update lan_spy.js
JonanOribe Feb 7, 2026
47ccc19
Styles update
JonanOribe Feb 7, 2026
7227ee5
Update lan_spy.js
JonanOribe Feb 7, 2026
1e08591
Update scanner.py
JonanOribe Feb 8, 2026
825e434
Styles for scan
JonanOribe Feb 9, 2026
b36311a
v2
JonanOribe Feb 9, 2026
23613ad
Update lan_spy_dashboard.css
JonanOribe Feb 9, 2026
5816a68
Update database.py
JonanOribe Feb 9, 2026
fc2f6f9
IP integration
JonanOribe Feb 10, 2026
b3c95cb
Front End revamp
JonanOribe Feb 10, 2026
3224bf5
Update
JonanOribe Feb 10, 2026
7508a52
Update
JonanOribe Feb 10, 2026
b3c633f
UX revamp
JonanOribe Feb 10, 2026
e1e758f
Styling
JonanOribe Feb 10, 2026
a70edfd
Update lan_spy.js
JonanOribe Feb 11, 2026
a664115
Update lan_spy.js
JonanOribe Feb 11, 2026
0ccff29
feat: wip changes before merging upstream
JonanOribe Feb 11, 2026
275a14f
Merge remote-tracking branch 'upstream/main' into lan-spy
JonanOribe Feb 11, 2026
8e0c26f
Update lan_spy.js
JonanOribe Feb 11, 2026
38bc682
Update Dockerfile
JonanOribe Feb 11, 2026
a5440aa
Merge branch 'main' into lan-spy
JonanOribe Feb 16, 2026
9bcd080
Update database.py
JonanOribe Feb 16, 2026
09abc0f
Update database.py
JonanOribe Feb 16, 2026
c8279e7
Merge branch 'main' into lan-spy
JonanOribe Feb 17, 2026
6d9884b
Merge branch 'main' into lan-spy
JonanOribe Feb 18, 2026
d4e986d
Merge branch 'main' into lan-spy
JonanOribe Feb 18, 2026
d0b9a89
Merge branch 'main' into lan-spy
JonanOribe Feb 21, 2026
6b22783
Update index.html
JonanOribe Feb 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Utilities
curl \
procps \
nmap \
&& rm -rf /var/lib/apt/lists/*

# Build dump1090-fa and acarsdec from source (packages not available in slim repos)
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Support the developer of this open-source project

```

> Note for Linux users: The LAN SPY network discovery requires `nmap` to be
> installed. On Debian/Ubuntu install with `sudo apt install nmap` before
> running `./setup.sh` or the LAN SPY scan will not work correctly.

**1. Clone and run:**
```bash
git clone https://github.com/smittix/intercept.git
Expand All @@ -67,7 +71,7 @@ cd intercept
sudo -E venv/bin/python intercept.py
```

### Docker
### Docker (Alternative)

```bash
git clone https://github.com/smittix/intercept.git
Expand Down
290 changes: 154 additions & 136 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
from flask_limiter.util import get_remote_address
# Track application start time for uptime calculation
import time as _time
from routes import lan_spy as lan_spy_module

_app_start_time = _time.time()
logger = logging.getLogger('intercept.database')

Expand Down Expand Up @@ -96,32 +98,32 @@ def add_security_headers(response):
# CONTEXT PROCESSORS
# ============================================

@app.context_processor
def inject_offline_settings():
"""Inject offline settings into all templates."""
from utils.database import get_setting

# Privacy-first defaults: keep dashboard assets/fonts local to avoid
# third-party tracker/storage defenses in strict browsers.
assets_source = str(get_setting('offline.assets_source', 'local') or 'local').lower()
fonts_source = str(get_setting('offline.fonts_source', 'local') or 'local').lower()
if assets_source not in ('local', 'cdn'):
assets_source = 'local'
if fonts_source not in ('local', 'cdn'):
fonts_source = 'local'
# Force local delivery for core dashboard pages.
assets_source = 'local'
fonts_source = 'local'

return {
'offline_settings': {
'enabled': get_setting('offline.enabled', False),
'assets_source': assets_source,
'fonts_source': fonts_source,
'tile_provider': get_setting('offline.tile_provider', 'cartodb_dark_cyan'),
'tile_server_url': get_setting('offline.tile_server_url', '')
}
}
@app.context_processor
def inject_offline_settings():
"""Inject offline settings into all templates."""
from utils.database import get_setting
# Privacy-first defaults: keep dashboard assets/fonts local to avoid
# third-party tracker/storage defenses in strict browsers.
assets_source = str(get_setting('offline.assets_source', 'local') or 'local').lower()
fonts_source = str(get_setting('offline.fonts_source', 'local') or 'local').lower()
if assets_source not in ('local', 'cdn'):
assets_source = 'local'
if fonts_source not in ('local', 'cdn'):
fonts_source = 'local'
# Force local delivery for core dashboard pages.
assets_source = 'local'
fonts_source = 'local'
return {
'offline_settings': {
'enabled': get_setting('offline.enabled', False),
'assets_source': assets_source,
'fonts_source': fonts_source,
'tile_provider': get_setting('offline.tile_provider', 'cartodb_dark_cyan'),
'tile_server_url': get_setting('offline.tile_server_url', '')
}
}


# ============================================
Expand Down Expand Up @@ -190,9 +192,9 @@ def inject_offline_settings():
dsc_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
dsc_lock = threading.Lock()

# TSCM (Technical Surveillance Countermeasures)
tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
tscm_lock = threading.Lock()
# TSCM (Technical Surveillance Countermeasures)
tscm_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
tscm_lock = threading.Lock()

# SubGHz Transceiver (HackRF)
subghz_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
Expand All @@ -203,6 +205,11 @@ def inject_offline_settings():
deauth_detector_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)
deauth_detector_lock = threading.Lock()

# Lan Spy (Network Device Discovery)
lan_spy_process = None
lan_spy_lock = threading.Lock()
lan_spy_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE)

# ============================================
# GLOBAL STATE DICTIONARIES
# ============================================
Expand Down Expand Up @@ -659,110 +666,112 @@ def export_bluetooth() -> Response:
})


def _get_subghz_active() -> bool:
"""Check if SubGHz manager has an active process."""
try:
from utils.subghz import get_subghz_manager
return get_subghz_manager().active_mode != 'idle'
except Exception:
return False


def _get_bluetooth_health() -> tuple[bool, int]:
"""Return Bluetooth active state and best-effort device count."""
legacy_running = bt_process is not None and (bt_process.poll() is None if bt_process else False)
scanner_running = False
scanner_count = 0

try:
from utils.bluetooth.scanner import _scanner_instance as bt_scanner
if bt_scanner is not None:
scanner_running = bool(bt_scanner.is_scanning)
scanner_count = int(bt_scanner.device_count)
except Exception:
scanner_running = False
scanner_count = 0

locate_running = False
try:
from utils.bt_locate import get_locate_session
session = get_locate_session()
if session and getattr(session, 'active', False):
scanner = getattr(session, '_scanner', None)
locate_running = bool(scanner and scanner.is_scanning)
except Exception:
locate_running = False

return (legacy_running or scanner_running or locate_running), max(len(bt_devices), scanner_count)


def _get_wifi_health() -> tuple[bool, int, int]:
"""Return WiFi active state and best-effort network/client counts."""
legacy_running = wifi_process is not None and (wifi_process.poll() is None if wifi_process else False)
scanner_running = False
scanner_networks = 0
scanner_clients = 0

try:
from utils.wifi.scanner import _scanner_instance as wifi_scanner
if wifi_scanner is not None:
status = wifi_scanner.get_status()
scanner_running = bool(status.is_scanning)
scanner_networks = int(status.networks_found or 0)
scanner_clients = int(status.clients_found or 0)
except Exception:
scanner_running = False
scanner_networks = 0
scanner_clients = 0

return (
legacy_running or scanner_running,
max(len(wifi_networks), scanner_networks),
max(len(wifi_clients), scanner_clients),
)


@app.route('/health')
def health_check() -> Response:
"""Health check endpoint for monitoring."""
import time
bt_active, bt_device_count = _get_bluetooth_health()
wifi_active, wifi_network_count, wifi_client_count = _get_wifi_health()
return jsonify({
'status': 'healthy',
'version': VERSION,
'uptime_seconds': round(time.time() - _app_start_time, 2),
'processes': {
def _get_subghz_active() -> bool:
"""Check if SubGHz manager has an active process."""
try:
from utils.subghz import get_subghz_manager
return get_subghz_manager().active_mode != 'idle'
except Exception:
return False
def _get_bluetooth_health() -> tuple[bool, int]:
"""Return Bluetooth active state and best-effort device count."""
legacy_running = bt_process is not None and (bt_process.poll() is None if bt_process else False)
scanner_running = False
scanner_count = 0
try:
from utils.bluetooth.scanner import _scanner_instance as bt_scanner
if bt_scanner is not None:
scanner_running = bool(bt_scanner.is_scanning)
scanner_count = int(bt_scanner.device_count)
except Exception:
scanner_running = False
scanner_count = 0
locate_running = False
try:
from utils.bt_locate import get_locate_session
session = get_locate_session()
if session and getattr(session, 'active', False):
scanner = getattr(session, '_scanner', None)
locate_running = bool(scanner and scanner.is_scanning)
except Exception:
locate_running = False
return (legacy_running or scanner_running or locate_running), max(len(bt_devices), scanner_count)
def _get_wifi_health() -> tuple[bool, int, int]:
"""Return WiFi active state and best-effort network/client counts."""
legacy_running = wifi_process is not None and (wifi_process.poll() is None if wifi_process else False)
scanner_running = False
scanner_networks = 0
scanner_clients = 0
try:
from utils.wifi.scanner import _scanner_instance as wifi_scanner
if wifi_scanner is not None:
status = wifi_scanner.get_status()
scanner_running = bool(status.is_scanning)
scanner_networks = int(status.networks_found or 0)
scanner_clients = int(status.clients_found or 0)
except Exception:
scanner_running = False
scanner_networks = 0
scanner_clients = 0
return (
legacy_running or scanner_running,
max(len(wifi_networks), scanner_networks),
max(len(wifi_clients), scanner_clients),
)
@app.route('/health')
def health_check() -> Response:
"""Health check endpoint for monitoring."""
import time
bt_active, bt_device_count = _get_bluetooth_health()
wifi_active, wifi_network_count, wifi_client_count = _get_wifi_health()
return jsonify({
'status': 'healthy',
'version': VERSION,
'uptime_seconds': round(time.time() - _app_start_time, 2),
'processes': {
'pager': current_process is not None and (current_process.poll() is None if current_process else False),
'sensor': sensor_process is not None and (sensor_process.poll() is None if sensor_process else False),
'adsb': adsb_process is not None and (adsb_process.poll() is None if adsb_process else False),
'ais': ais_process is not None and (ais_process.poll() is None if ais_process else False),
'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False),
'vdl2': vdl2_process is not None and (vdl2_process.poll() is None if vdl2_process else False),
'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False),
'wifi': wifi_active,
'bluetooth': bt_active,
'dsc': dsc_process is not None and (dsc_process.poll() is None if dsc_process else False),
'subghz': _get_subghz_active(),
},
'data': {
'aircraft_count': len(adsb_aircraft),
'vessel_count': len(ais_vessels),
'wifi_networks_count': wifi_network_count,
'wifi_clients_count': wifi_client_count,
'bt_devices_count': bt_device_count,
'dsc_messages_count': len(dsc_messages),
}
})
'ais': ais_process is not None and (ais_process.poll() is None if ais_process else False),
'acars': acars_process is not None and (acars_process.poll() is None if acars_process else False),
'vdl2': vdl2_process is not None and (vdl2_process.poll() is None if vdl2_process else False),
'aprs': aprs_process is not None and (aprs_process.poll() is None if aprs_process else False),
'wifi': wifi_active,
'bluetooth': bt_active,
'dsc': dsc_process is not None and (dsc_process.poll() is None if dsc_process else False),
'subghz': _get_subghz_active(),
'lan_spy': lan_spy_process is not None and (lan_spy_process.poll() is None if lan_spy_process else False)
},
'data': {
'aircraft_count': len(adsb_aircraft),
'vessel_count': len(ais_vessels),
'wifi_networks_count': wifi_network_count,
'wifi_clients_count': wifi_client_count,
'bt_devices_count': bt_device_count,
'dsc_messages_count': len(dsc_messages),
'lan_devices_count': len(lan_spy_module.lan_devices)
}
})


@app.route('/killall', methods=['POST'])
def kill_all() -> Response:
def kill_all() -> Response:
"""Kill all decoder, WiFi, and Bluetooth processes."""
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
global vdl2_process
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process

global lan_spy_process
# Import adsb and ais modules to reset their state
from routes import adsb as adsb_module
from routes import ais as ais_module
Expand All @@ -773,9 +782,9 @@ def kill_all() -> Response:
'rtl_fm', 'multimon-ng', 'rtl_433',
'airodump-ng', 'aireplay-ng', 'airmon-ng',
'dump1090', 'acarsdec', 'dumpvdl2', 'direwolf', 'AIS-catcher',
'hcitool', 'bluetoothctl', 'satdump',
'hcitool', 'bluetoothctl', 'satdump',
'rtl_tcp', 'rtl_power', 'rtlamr', 'ffmpeg',
'hackrf_transfer', 'hackrf_sweep'
'hackrf_transfer', 'hackrf_sweep','nmap', 'arp-scan'
]

for proc in processes_to_kill:
Expand All @@ -786,6 +795,15 @@ def kill_all() -> Response:
except (subprocess.SubprocessError, OSError):
pass

with lan_spy_lock:
if lan_spy_process:
try:
lan_spy_process.terminate()
except:
pass
lan_spy_process = None
lan_spy_module.lan_spy_scan_running = False

with process_lock:
current_process = None

Expand Down Expand Up @@ -823,7 +841,7 @@ def kill_all() -> Response:
dsc_process = None
dsc_rtl_process = None

# Reset Bluetooth state (legacy)
# Reset Bluetooth state (legacy)
with bt_lock:
if bt_process:
try:
Expand All @@ -843,16 +861,16 @@ def kill_all() -> Response:
except Exception:
pass

# Reset SubGHz state
try:
from utils.subghz import get_subghz_manager
get_subghz_manager().stop_all()
except Exception:
pass

# Clear SDR device registry
with sdr_device_registry_lock:
sdr_device_registry.clear()
# Reset SubGHz state
try:
from utils.subghz import get_subghz_manager
get_subghz_manager().stop_all()
except Exception:
pass
# Clear SDR device registry
with sdr_device_registry_lock:
sdr_device_registry.clear()

return jsonify({'status': 'killed', 'processes': killed})

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies = [
"flask-sock",
"websocket-client>=1.6.0",
"requests>=2.28.0",
"pyyaml>=6.0"
]

[project.urls]
Expand Down
Loading