Refactor into modular structure with improvements

- Split monolithic intercept.py (15k lines) into modular structure:
  - routes/ - Flask blueprints for each feature
  - templates/ - Jinja2 HTML templates
  - data/ - OUI database, satellite TLEs, detection patterns
  - utils/ - dependencies, process management, logging
  - config.py - centralized configuration with env var support

- Add type hints to function signatures
- Replace bare except clauses with specific exceptions
- Add proper logging module (replaces print statements)
- Add environment variable support (INTERCEPT_* prefix)
- Add test suite with pytest
- Add Dockerfile for containerized deployment
- Add pyproject.toml with ruff/black/mypy config
- Add requirements-dev.txt for development dependencies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
James Smith
2025-12-23 16:28:36 +00:00
parent b0bc7e6e91
commit ddeff002c9
32 changed files with 15581 additions and 15429 deletions

14
utils/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
# Utility modules for INTERCEPT
from .dependencies import check_tool, check_all_dependencies, TOOL_DEPENDENCIES
from .process import cleanup_stale_processes, is_valid_mac, is_valid_channel, detect_devices
from .logging import (
get_logger,
app_logger,
pager_logger,
sensor_logger,
wifi_logger,
bluetooth_logger,
adsb_logger,
satellite_logger,
iridium_logger,
)

246
utils/dependencies.py Normal file
View File

@@ -0,0 +1,246 @@
from __future__ import annotations
import logging
import shutil
from typing import Any
logger = logging.getLogger('intercept.dependencies')
def check_tool(name: str) -> bool:
"""Check if a tool is installed."""
return shutil.which(name) is not None
# Comprehensive tool dependency definitions
TOOL_DEPENDENCIES = {
'pager': {
'name': 'Pager Decoding',
'tools': {
'rtl_fm': {
'required': True,
'description': 'RTL-SDR FM demodulator',
'install': {
'apt': 'sudo apt install rtl-sdr',
'brew': 'brew install librtlsdr',
'manual': 'https://osmocom.org/projects/rtl-sdr/wiki'
}
},
'multimon-ng': {
'required': True,
'description': 'Digital transmission decoder',
'install': {
'apt': 'sudo apt install multimon-ng',
'brew': 'brew install multimon-ng',
'manual': 'https://github.com/EliasOewornal/multimon-ng'
}
},
'rtl_test': {
'required': False,
'description': 'RTL-SDR device detection',
'install': {
'apt': 'sudo apt install rtl-sdr',
'brew': 'brew install librtlsdr',
'manual': 'https://osmocom.org/projects/rtl-sdr/wiki'
}
}
}
},
'sensor': {
'name': '433MHz Sensors',
'tools': {
'rtl_433': {
'required': True,
'description': 'ISM band decoder for sensors, weather stations, TPMS',
'install': {
'apt': 'sudo apt install rtl-433',
'brew': 'brew install rtl_433',
'manual': 'https://github.com/merbanan/rtl_433'
}
}
}
},
'wifi': {
'name': 'WiFi Reconnaissance',
'tools': {
'airmon-ng': {
'required': True,
'description': 'Monitor mode controller',
'install': {
'apt': 'sudo apt install aircrack-ng',
'brew': 'Not available on macOS',
'manual': 'https://aircrack-ng.org'
}
},
'airodump-ng': {
'required': True,
'description': 'WiFi network scanner',
'install': {
'apt': 'sudo apt install aircrack-ng',
'brew': 'Not available on macOS',
'manual': 'https://aircrack-ng.org'
}
},
'aireplay-ng': {
'required': False,
'description': 'Deauthentication / packet injection',
'install': {
'apt': 'sudo apt install aircrack-ng',
'brew': 'Not available on macOS',
'manual': 'https://aircrack-ng.org'
}
},
'aircrack-ng': {
'required': False,
'description': 'Handshake verification',
'install': {
'apt': 'sudo apt install aircrack-ng',
'brew': 'brew install aircrack-ng',
'manual': 'https://aircrack-ng.org'
}
},
'hcxdumptool': {
'required': False,
'description': 'PMKID capture tool',
'install': {
'apt': 'sudo apt install hcxdumptool',
'brew': 'brew install hcxtools',
'manual': 'https://github.com/ZerBea/hcxdumptool'
}
},
'hcxpcapngtool': {
'required': False,
'description': 'PMKID hash extractor',
'install': {
'apt': 'sudo apt install hcxtools',
'brew': 'brew install hcxtools',
'manual': 'https://github.com/ZerBea/hcxtools'
}
}
}
},
'bluetooth': {
'name': 'Bluetooth Scanning',
'tools': {
'hcitool': {
'required': False,
'description': 'Bluetooth HCI tool (legacy)',
'install': {
'apt': 'sudo apt install bluez',
'brew': 'Not available on macOS (use native)',
'manual': 'http://www.bluez.org'
}
},
'bluetoothctl': {
'required': True,
'description': 'Modern Bluetooth controller',
'install': {
'apt': 'sudo apt install bluez',
'brew': 'Not available on macOS (use native)',
'manual': 'http://www.bluez.org'
}
},
'hciconfig': {
'required': False,
'description': 'Bluetooth adapter configuration',
'install': {
'apt': 'sudo apt install bluez',
'brew': 'Not available on macOS',
'manual': 'http://www.bluez.org'
}
}
}
},
'aircraft': {
'name': 'Aircraft Tracking (ADS-B)',
'tools': {
'dump1090': {
'required': False,
'description': 'Mode S / ADS-B decoder (preferred)',
'install': {
'apt': 'sudo apt install dump1090-mutability',
'brew': 'brew install dump1090-mutability',
'manual': 'https://github.com/flightaware/dump1090'
},
'alternatives': ['dump1090-mutability', 'dump1090-fa']
},
'rtl_adsb': {
'required': False,
'description': 'Simple ADS-B decoder',
'install': {
'apt': 'sudo apt install rtl-sdr',
'brew': 'brew install librtlsdr',
'manual': 'https://osmocom.org/projects/rtl-sdr/wiki'
}
}
}
},
'satellite': {
'name': 'Satellite Tracking',
'tools': {
'skyfield': {
'required': True,
'description': 'Python orbital mechanics library',
'install': {
'pip': 'pip install skyfield',
'manual': 'https://rhodesmill.org/skyfield/'
},
'python_module': True
}
}
},
'iridium': {
'name': 'Iridium Monitoring',
'tools': {
'iridium-extractor': {
'required': False,
'description': 'Iridium burst extractor',
'install': {
'manual': 'https://github.com/muccc/gr-iridium'
}
}
}
}
}
def check_all_dependencies() -> dict[str, dict[str, Any]]:
"""Check all tool dependencies and return status."""
results: dict[str, dict[str, Any]] = {}
for mode, config in TOOL_DEPENDENCIES.items():
mode_result = {
'name': config['name'],
'tools': {},
'ready': True,
'missing_required': []
}
for tool, tool_config in config['tools'].items():
# Check if it's a Python module
if tool_config.get('python_module'):
try:
__import__(tool)
installed = True
except Exception as e:
logger.debug(f"Failed to import {tool}: {type(e).__name__}: {e}")
installed = False
else:
# Check for alternatives
alternatives = tool_config.get('alternatives', [])
installed = check_tool(tool) or any(check_tool(alt) for alt in alternatives)
mode_result['tools'][tool] = {
'installed': installed,
'required': tool_config['required'],
'description': tool_config['description'],
'install': tool_config['install']
}
if tool_config['required'] and not installed:
mode_result['ready'] = False
mode_result['missing_required'].append(tool)
results[mode] = mode_result
return results

30
utils/logging.py Normal file
View File

@@ -0,0 +1,30 @@
"""Logging utilities for intercept application."""
from __future__ import annotations
import logging
import sys
from config import LOG_LEVEL, LOG_FORMAT
def get_logger(name: str) -> logging.Logger:
"""Get a configured logger for a module."""
logger = logging.getLogger(name)
if not logger.handlers:
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter(LOG_FORMAT))
logger.addHandler(handler)
logger.setLevel(LOG_LEVEL)
return logger
# Pre-configured loggers for each module
app_logger = get_logger('intercept')
pager_logger = get_logger('intercept.pager')
sensor_logger = get_logger('intercept.sensor')
wifi_logger = get_logger('intercept.wifi')
bluetooth_logger = get_logger('intercept.bluetooth')
adsb_logger = get_logger('intercept.adsb')
satellite_logger = get_logger('intercept.satellite')
iridium_logger = get_logger('intercept.iridium')

80
utils/process.py Normal file
View File

@@ -0,0 +1,80 @@
from __future__ import annotations
import subprocess
import re
from typing import Any
from .dependencies import check_tool
def cleanup_stale_processes() -> None:
"""Kill any stale processes from previous runs (but not system services)."""
# Note: dump1090 is NOT included here as users may run it as a system service
processes_to_kill = ['rtl_adsb', 'rtl_433', 'multimon-ng', 'rtl_fm']
for proc_name in processes_to_kill:
try:
subprocess.run(['pkill', '-9', proc_name], capture_output=True)
except (subprocess.SubprocessError, OSError):
pass
def is_valid_mac(mac: str | None) -> bool:
"""Validate MAC address format."""
if not mac:
return False
return bool(re.match(r'^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$', mac))
def is_valid_channel(channel: str | int | None) -> bool:
"""Validate WiFi channel number."""
try:
ch = int(channel) # type: ignore[arg-type]
return 1 <= ch <= 200
except (ValueError, TypeError):
return False
def detect_devices() -> list[dict[str, Any]]:
"""Detect RTL-SDR devices."""
devices: list[dict[str, Any]] = []
if not check_tool('rtl_test'):
return devices
try:
result = subprocess.run(
['rtl_test', '-t'],
capture_output=True,
text=True,
timeout=5
)
output = result.stderr + result.stdout
# Parse device info
device_pattern = r'(\d+):\s+(.+?)(?:,\s*SN:\s*(\S+))?$'
for line in output.split('\n'):
line = line.strip()
match = re.match(device_pattern, line)
if match:
devices.append({
'index': int(match.group(1)),
'name': match.group(2).strip().rstrip(','),
'serial': match.group(3) or 'N/A'
})
if not devices:
found_match = re.search(r'Found (\d+) device', output)
if found_match:
count = int(found_match.group(1))
for i in range(count):
devices.append({
'index': i,
'name': f'RTL-SDR Device {i}',
'serial': 'Unknown'
})
except Exception:
pass
return devices