diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ab54a1e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,272 @@ +# Security Policy + +## Overview + +ConnectivityMonitor is designed for local network monitoring and diagnostics. This document outlines security considerations, best practices, and how to report security vulnerabilities. + +## Security Model + +ConnectivityMonitor is designed to run on: +- **Local machines** for personal network diagnostics +- **Private networks** behind firewalls +- **Trusted environments** where users have appropriate access + +### Threat Model + +**What ConnectivityMonitor protects against:** +- Command injection in network operations +- Path traversal in file serving +- Code injection via configuration files +- Insecure temporary file creation + +**What ConnectivityMonitor does NOT protect against:** +- Unauthorized access to the web dashboard (no authentication) +- Data exfiltration if the host is compromised +- Network-level attacks on the monitoring traffic itself + +## Security Best Practices + +### Web Dashboard Security + +The built-in web server (Python version) **does not include authentication** by default. Follow these guidelines: + +#### For Local Use Only (Recommended) +```bash +# Bind to localhost only (default behavior can be changed) +python -m connectivity_monitor --web-port 8080 +# Access via http://localhost:8080 only +``` + +#### For Network Access +If you need to access the dashboard from other devices on your network: + +1. **Firewall Configuration**: Ensure your firewall only allows connections from trusted IPs +2. **Bind Address**: By default, the server binds to `0.0.0.0` (all interfaces) +3. **Network Isolation**: Run only on trusted private networks (home/office LAN) +4. **VPN Access**: For remote access, use a VPN instead of exposing the port publicly + +**⚠️ WARNING**: Do NOT expose the web dashboard to the public internet without additional security measures. + +### Configuration File Security + +Configuration files are stored at: +- **Linux/macOS**: `~/ConnectivityMonitor/monitor_config.json` +- **Windows**: `%USERPROFILE%\ConnectivityMonitor\monitor_config.json` + +**Best practices:** +- Keep configuration files with appropriate permissions (readable only by owner) +- Do not store sensitive credentials in configuration files +- Review ping targets to ensure they are legitimate and trusted + +### Execution Policy (Windows) + +The PowerShell script requires script execution to be enabled: + +```powershell +# Check current execution policy +Get-ExecutionPolicy + +# Set execution policy for current user (recommended) +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +# Alternative: Run with bypass for single session +powershell -ExecutionPolicy Bypass -File .\ConnectivityDropMonitor.ps1 +``` + +**Security note**: Only use `RemoteSigned` or stricter policies. Avoid `Unrestricted` in production environments. + +### macOS/Linux Permissions + +**Script Permissions:** +```bash +# Set appropriate execute permissions +chmod 755 macos/ConnectivityDropMonitor.sh +chmod 644 macos/lib/*.sh # Library files don't need execute bit + +# Ensure ownership is correct +chown -R $USER:$GROUP ~/ConnectivityMonitor +``` + +**File Permissions:** +```bash +# Secure the base directory +chmod 700 ~/ConnectivityMonitor + +# Logs and reports +chmod 600 ~/ConnectivityMonitor/logs/*.csv +chmod 600 ~/ConnectivityMonitor/reports/*.html +``` + +### Network Security Considerations + +**ICMP Ping Requirements:** +- Ping commands typically require no special privileges on Windows +- On Linux/macOS, ping may require setuid or capabilities +- The monitor uses system `ping` command, not raw sockets + +**DNS Security:** +- DNS health checks query configured DNS servers +- Ensure DNS targets are trusted (default: google.com) +- DNS spoofing could affect diagnostics but not monitoring integrity + +**External API Calls:** +The monitor makes HTTPS calls to: +- `https://ip-api.com/json` — Public IP and ISP detection + +These are optional features. If privacy is a concern: +1. Review the code in `network.py` (Python), `lib/network.sh` (macOS), or PowerShell functions +2. These calls happen once at startup (not continuously) +3. No personally identifiable information is sent (simple GET request) + +### Data Privacy + +**What data is collected:** +- Ping latency measurements +- Packet loss statistics +- Gateway IP addresses +- Public IP address (via ip-api.com) +- ISP name (via ip-api.com) +- WiFi signal strength (if available) + +**What is NOT collected:** +- Personal user information +- Browsing history +- Network traffic content +- Authentication credentials + +**Data storage:** +- All data is stored locally in CSV and HTML files +- No telemetry is sent to external services +- Logs can be deleted at any time + +### Environment Variables + +The monitor supports customization via environment variables: + +```bash +# Customize base directory location +export CM_BASE_DIR="$HOME/my-custom-location" +python -m connectivity_monitor +``` + +**Security consideration**: Ensure custom directories have appropriate permissions. + +## Known Limitations + +1. **No Authentication**: Web dashboard has no built-in authentication +2. **No Encryption**: Local file storage is unencrypted +3. **No Rate Limiting**: Web API endpoints have no rate limiting +4. **No Audit Logging**: No security event logging beyond diagnostic logs + +## Security Fixes in v4.0 + +This release includes the following security improvements: + +### Fixed Vulnerabilities +1. **Command Injection (PowerShell)** — Fixed in commit `13b4514` + - Old: Used `cmd /c "tracert $target"` (injectable) + - New: Uses `System.Diagnostics.ProcessStartInfo` with argument array + +2. **HTTP Downgrade Attack** — Fixed in commit `13b4514` + - Old: Used `http://ip-api.com` (unencrypted) + - New: Uses `https://ip-api.com` (encrypted) + +3. **Path Traversal (Python Web Server)** — Fixed in commit `13b4514` + - Old: Insufficient validation with `basename()` only + - New: Validates resolved path stays within base directory + +4. **Code Injection (Bash Config)** — Fixed in commit `13b4514` + - Old: Interpolated shell variables into Python code strings + - New: Uses heredoc with command-line arguments (safe) + +5. **Insecure Temporary Files (macOS)** — Fixed in commit `13b4514` + - Old: Predictable filenames `/tmp/cm_traceroute_$$.txt` + - New: Uses `mktemp` with random names + +### Input Validation Improvements (commit `25b672a`) +- Poll interval: Must be between 0.1 and 3600 seconds +- Failure threshold: Must be between 1 and 100 +- Web port: Must be between 1 and 65535 +- Ping targets: Validated as IP addresses or hostnames +- DNS target: Validated as valid hostname + +## Reporting a Security Vulnerability + +If you discover a security vulnerability in ConnectivityMonitor: + +1. **Do NOT** open a public GitHub issue +2. **Email** the maintainer: Include the following in your report: + - Description of the vulnerability + - Steps to reproduce + - Potential impact + - Suggested fix (if available) + +3. **Allow time** for a fix to be developed and released before public disclosure + +### Security Contact + +For security issues, please open a security advisory on GitHub or create an issue with the `security` label. The maintainer will respond within 7 days. + +## Security Checklist for Users + +Before deploying ConnectivityMonitor in a production environment: + +- [ ] Review all ping targets and ensure they are trusted +- [ ] Configure firewall rules to restrict web dashboard access +- [ ] Set appropriate file permissions on logs and config files +- [ ] Run with least privileges (do not run as root/administrator unless required) +- [ ] Keep the software updated to receive security fixes +- [ ] Review logs periodically for unexpected behavior +- [ ] Disable features you don't need (e.g., DNS checks, public IP detection) +- [ ] Use a separate monitoring account with limited privileges +- [ ] Consider network segmentation if running on servers + +## Security-Related Configuration + +### Disable Public IP Detection + +If privacy is a concern, you can disable public IP detection by modifying the code: + +**Python**: Comment out the `detect_public_ip()` call in `monitor.py` +**macOS**: Comment out the `detect_public_ip` call in `ConnectivityDropMonitor.sh` +**Windows**: Comment out the `DetectPublicIP` call in `ConnectivityDropMonitor.ps1` + +### Restrict Web Dashboard Binding + +**Python**: Modify `web_server.py` to bind to `127.0.0.1` instead of `0.0.0.0`: + +```python +def start_web_server(port, state, reports_dir, logs_dir, bind="127.0.0.1"): +``` + +### Read-Only Logs + +To prevent log tampering: + +```bash +# Make logs read-only after rotation +chmod 444 ~/ConnectivityMonitor/logs/*.csv +``` + +## Compliance and Certifications + +ConnectivityMonitor is not certified for: +- Medical use +- Financial services +- Critical infrastructure +- Compliance with specific regulations (HIPAA, PCI-DSS, etc.) + +For compliance-critical environments, perform your own security audit and testing. + +## License and Disclaimer + +This software is provided as-is without warranty. Users are responsible for: +- Evaluating security for their specific use case +- Implementing additional security controls as needed +- Monitoring and maintaining the software +- Complying with applicable laws and regulations + +--- + +**Last Updated**: 2026-04-06 +**Document Version**: 1.0 diff --git a/macos/lib/config.sh b/macos/lib/config.sh index dd739af..1f63411 100755 --- a/macos/lib/config.sh +++ b/macos/lib/config.sh @@ -34,7 +34,7 @@ check_date_roll() { # ================================================================ load_config() { if [[ -f "$CM_CONFIG_PATH" ]]; then - if python3 -c "import json; json.load(open('$CM_CONFIG_PATH'))" 2>/dev/null; then + if python3 -c "import json; json.load(open(\"${CM_CONFIG_PATH}\"))" 2>/dev/null; then return 0 fi fi @@ -43,27 +43,52 @@ load_config() { config_get() { local key="$1" - python3 -c "import json; d=json.load(open('${CM_CONFIG_PATH}')); print(d.get('${key}',''))" 2>/dev/null + # Use printf to safely pass key to Python, avoiding code injection + python3 -c " +import json +import sys +key = sys.argv[1] +with open(\"${CM_CONFIG_PATH}\", 'r') as f: + d = json.load(f) + print(d.get(key, '')) +" "$key" 2>/dev/null } save_config() { local cfg_adapter="$1" cfg_poll="$2" cfg_threshold="$3" cfg_targets="$4" local cfg_latwarn="$5" cfg_enabledns="$6" cfg_dnstarget="$7" cfg_enablebeep="$8" - python3 -c " + # Use Python script file to avoid shell injection vulnerabilities + python3 - "$cfg_adapter" "$cfg_poll" "$cfg_threshold" "$cfg_targets" \ + "$cfg_latwarn" "$cfg_enabledns" "$cfg_dnstarget" "$cfg_enablebeep" \ + "${CM_CONFIG_PATH}" <<'PYTHON_SCRIPT' import json +import sys + +# Read arguments from command line +cfg_adapter = sys.argv[1] +cfg_poll = int(sys.argv[2]) +cfg_threshold = int(sys.argv[3]) +cfg_targets = sys.argv[4] +cfg_latwarn = int(sys.argv[5]) +cfg_enabledns = sys.argv[6].lower() in ('1', 'true') +cfg_dnstarget = sys.argv[7] +cfg_enablebeep = sys.argv[8].lower() in ('1', 'true') +config_path = sys.argv[9] + d = { - 'adapter': '${cfg_adapter}', - 'poll': ${cfg_poll}, - 'threshold': ${cfg_threshold}, - 'targets': '${cfg_targets}', - 'latWarn': ${cfg_latwarn}, - 'enableDns': bool(${cfg_enabledns}), - 'dnsTarget': '${cfg_dnstarget}', - 'enableBeep': bool(${cfg_enablebeep}) + 'adapter': cfg_adapter, + 'poll': cfg_poll, + 'threshold': cfg_threshold, + 'targets': cfg_targets, + 'latWarn': cfg_latwarn, + 'enableDns': cfg_enabledns, + 'dnsTarget': cfg_dnstarget, + 'enableBeep': cfg_enablebeep } -with open('${CM_CONFIG_PATH}', 'w') as f: + +with open(config_path, 'w') as f: json.dump(d, f, indent=2) -" 2>/dev/null +PYTHON_SCRIPT } # ================================================================ diff --git a/macos/lib/network.sh b/macos/lib/network.sh index 99e2872..4b1a19c 100755 --- a/macos/lib/network.sh +++ b/macos/lib/network.sh @@ -78,7 +78,7 @@ get_local_ip() { # ================================================================ detect_public_ip() { local resp - resp=$(curl -s --connect-timeout 5 --max-time 8 "http://ip-api.com/json" 2>/dev/null) + resp=$(curl -s --connect-timeout 5 --max-time 8 "https://ip-api.com/json" 2>/dev/null) if [[ -n "$resp" ]]; then public_ip=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('query','N/A'))" 2>/dev/null) isp_name=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('isp','N/A'))" 2>/dev/null) diff --git a/macos/lib/state.sh b/macos/lib/state.sh index 6b19172..11ff809 100755 --- a/macos/lib/state.sh +++ b/macos/lib/state.sh @@ -2,79 +2,143 @@ # ============================================================================ # STATE MODULE - Global state variables # Connectivity Monitor v4.0 (macOS) +# +# This module defines all global variables used throughout the monitoring +# session. Variables are organized into logical groups for clarity. +# +# IMPORTANT: This file must be sourced before all other library modules. +# +# Global Variable Groups: +# - History arrays: Store ping results over time (circular buffer, max 1000) +# - Drop tracking: Record connectivity drop events +# - Per-target stats: Statistics per ping target (associative arrays) +# - Gateway tracking: Monitor gateway latency separately +# - Baseline learning: Learn normal latency from first 30 pings +# - Hourly heatmap: Aggregate latency by hour of day +# - Traceroute data: Store recent traceroute results +# - Session state: Counters, flags, timestamps +# - Directory paths: Base directory, logs, reports, config # ============================================================================ -# History arrays (parallel arrays for timestamp, latency, target) +# ============================================================================ +# HISTORY ARRAYS - Recent ping results (circular buffer) +# ============================================================================ +# Parallel arrays storing ping history (max 1000 entries): +# - hist_times: Unix timestamps of ping attempts +# - hist_lats: Latency values in ms (empty string for failed pings) +# - hist_targets: Target IP/hostname for each ping declare -a hist_times=() declare -a hist_lats=() declare -a hist_targets=() -# Drop arrays +# ============================================================================ +# DROP TRACKING - Connectivity outage events +# ============================================================================ +# Parallel arrays storing drop events: +# - drop_starts: ISO 8601 timestamp when drop started +# - drop_ends: ISO 8601 timestamp when connectivity restored +# - drop_durations: Duration in seconds +# - drop_targets: Target being monitored when drop occurred +# - drop_diagnoses: Diagnostic message (e.g., "Gateway unreachable") declare -a drop_starts=() declare -a drop_ends=() declare -a drop_durations=() declare -a drop_targets=() declare -a drop_diagnoses=() -# Per-target stats (associative arrays) +# ============================================================================ +# PER-TARGET STATISTICS - Associative arrays keyed by target IP/hostname +# ============================================================================ +# Track statistics for each ping target independently: +# - per_target_sent: Number of pings sent to this target +# - per_target_ok: Number of successful pings +# - per_target_lats: Comma-separated latency values declare -A per_target_sent=() declare -A per_target_ok=() declare -A per_target_lats=() -# Gateway history +# ============================================================================ +# GATEWAY TRACKING +# ============================================================================ +# Gateway latency history (separate from main ping targets) declare -a gw_lats=() -# Threshold breach arrays +# ============================================================================ +# LATENCY THRESHOLD BREACH TRACKING +# ============================================================================ +# Track periods when latency exceeds configured threshold: +# - breach_starts: ISO timestamp when breach started +# - breach_ends: ISO timestamp when latency returned to normal +# - breach_durations: Duration in seconds +# - breach_avglats: Average latency during breach period declare -a breach_starts=() declare -a breach_ends=() declare -a breach_durations=() declare -a breach_avglats=() -# Baseline learning +# ============================================================================ +# BASELINE LEARNING +# ============================================================================ +# Store first 30 successful ping latencies to establish baseline declare -a baseline_samples=() -# Hourly data (associative: hour -> comma-separated latencies) +# ============================================================================ +# HOURLY HEATMAP DATA +# ============================================================================ +# Associative array: hour (0-23) -> comma-separated latencies +# Used to generate 24-hour heatmap showing time-of-day patterns declare -A hourly_data=() -# Traceroute entries (each entry is "target|time|hop_data") +# ============================================================================ +# TRACEROUTE DATA +# ============================================================================ +# Array of recent traceroute results (max 3 entries) +# Each entry format: "target|timestamp|hop_data" declare -a trace_entries=() -# Counters and flags -fail_count=0 -is_down=0 -down_start="" -session_start=$(date +%s) -total_pings=0 -total_success=0 -shutdown_flag=0 -max_history=1000 -last_line_count=0 -paused=0 -baseline_latency="" -baseline_locked=0 -public_ip="detecting..." -isp_name="detecting..." -active_tab=1 -breach_active=0 -breach_start="" -current_date=$(date +%Y-%m-%d) -last_diagnosis_msg="Initializing..." -last_diagnosis_color="90" -last_wifi_sig="" -last_trace_time=0 -trace_pid="" -trace_target="" -trace_outfile="/tmp/cm_traceroute_$$.txt" -rr=0 -enable_beep=0 +# ============================================================================ +# SESSION STATE - Counters and flags +# ============================================================================ +fail_count=0 # Consecutive failed pings +is_down=0 # Boolean: currently in a drop state (0=up, 1=down) +down_start="" # ISO timestamp when current drop started +session_start=$(date +%s) # Unix timestamp of session start +total_pings=0 # Total ping attempts this session +total_success=0 # Total successful pings this session +shutdown_flag=0 # Boolean: user requested shutdown (0=run, 1=exit) +max_history=1000 # Maximum ping history entries (circular buffer) +last_line_count=0 # Terminal line count for display updates +paused=0 # Boolean: monitoring paused (0=running, 1=paused) +baseline_latency="" # Calculated baseline latency (ms) after 30 samples +baseline_locked=0 # Boolean: baseline calculation complete +public_ip="detecting..." # Public IP address (from ip-api.com) +isp_name="detecting..." # ISP name (from ip-api.com) +active_tab=1 # Currently displayed dashboard tab (1-5) +breach_active=0 # Boolean: currently in latency threshold breach +breach_start="" # ISO timestamp of current breach start +current_date=$(date +%Y-%m-%d) # Current date for log rotation detection +last_diagnosis_msg="Initializing..." # Last diagnostic message +last_diagnosis_color="90" # ANSI color code for last diagnosis +last_wifi_sig="" # Last WiFi signal strength percentage +last_trace_time=0 # Unix timestamp of last traceroute invocation +trace_pid="" # PID of background traceroute process +trace_target="" # Target for current/last traceroute +# Use mktemp for secure temporary file creation +trace_outfile=$(mktemp 2>/dev/null) || trace_outfile="/tmp/cm_traceroute_$$_${RANDOM}.txt" +rr=0 # Round-robin index for target rotation +enable_beep=0 # Boolean: audible beep on drops (0=off, 1=on) -# Directory setup +# ============================================================================ +# DIRECTORY PATHS +# ============================================================================ CM_BASE_DIR="$HOME/Documents/ConnectivityMonitor" CM_LOGS_DIR="$CM_BASE_DIR/logs" CM_REPORTS_DIR="$CM_BASE_DIR/reports" CM_CONFIG_PATH="$CM_BASE_DIR/monitor_config.json" -# Log file paths (set by get_daily_log_paths) +# ============================================================================ +# LOG FILE PATHS (set by get_daily_log_paths function) +# ============================================================================ ping_log_file="" drop_log_file="" breach_log_file="" diff --git a/python/connectivity_monitor/__init__.py b/python/connectivity_monitor/__init__.py index d9a4773..65ceff0 100644 --- a/python/connectivity_monitor/__init__.py +++ b/python/connectivity_monitor/__init__.py @@ -4,5 +4,40 @@ # Works on Linux, macOS, Raspberry Pi — Python 3.6+ stdlib only # JAMES COATES and Claude Opus 4.6 (Graciously hosted with GitHub CoPilot) # ============================================================================ +""" +Connectivity Monitor — Real-time network monitoring with latency tracking + +A cross-platform network monitoring tool that continuously pings targets, +tracks latency and packet loss, detects outages, and provides live analytics +through a web dashboard. + +Key Features: +- Continuous ICMP ping monitoring with configurable targets +- Live web dashboard with auto-refresh (5 tabs: Overview, Graph, Drops, Targets, Heatmap) +- Smart diagnostics (local network, gateway, ISP, DNS health checks) +- Health scoring (A+ to F based on latency, jitter, packet loss) +- Baseline learning (learns normal latency from first 30 pings) +- Automatic traceroute on connectivity drops +- CSV logging with daily rotation (ping results, drops, latency breaches) +- HTML report generation with Chart.js visualizations +- Hourly heatmap for time-of-day pattern analysis + +Modules: +- config: Configuration management (JSON file, CLI prompts, validation) +- state: Global monitor state (all mutable session data) +- network: Network utilities (ping, DNS, gateway detection, traceroute) +- metrics: Statistical calculations (loss, avg, percentile, jitter, health score) +- csv_logger: CSV file logging with daily rotation +- html_report: HTML report generation with Chart.js +- web_server: Built-in HTTP server for live dashboard and JSON API +- monitor: Main monitoring loop and orchestration + +Usage: + python -m connectivity_monitor # Interactive setup + python -m connectivity_monitor --headless # Headless mode with saved config + python -m connectivity_monitor --targets 1.1.1.1,8.8.8.8 --web-port 9090 + +See README.md and docs/ directory for full documentation. +""" __version__ = "4.0" diff --git a/python/connectivity_monitor/__main__.py b/python/connectivity_monitor/__main__.py index ce620a1..ef711f6 100644 --- a/python/connectivity_monitor/__main__.py +++ b/python/connectivity_monitor/__main__.py @@ -3,6 +3,12 @@ import argparse import sys +# Check Python version before importing other modules +if sys.version_info < (3, 6): + sys.exit("Error: Python 3.6+ is required. You are using Python {}.{}.{}".format( + sys.version_info.major, sys.version_info.minor, sys.version_info.micro + )) + from .config import load_config, interactive_setup, headless_config, ensure_dirs from .monitor import run_monitor diff --git a/python/connectivity_monitor/config.py b/python/connectivity_monitor/config.py index c6b5aed..92b2c82 100644 --- a/python/connectivity_monitor/config.py +++ b/python/connectivity_monitor/config.py @@ -2,6 +2,7 @@ import json import os +import re import sys @@ -19,6 +20,10 @@ def get_base_dir(): """Return base directory for logs/reports/config.""" + # Check environment variable first for user customization + base = os.getenv("CM_BASE_DIR") + if base: + return os.path.expanduser(base) home = os.path.expanduser("~") return os.path.join(home, "ConnectivityMonitor") @@ -44,7 +49,9 @@ def load_config(): try: with open(path, "r") as f: return json.load(f) - except Exception: + except (json.JSONDecodeError, IOError, OSError) as e: + # Invalid JSON or file read errors + print(f"Warning: Could not load config from {path}: {e}", file=sys.stderr) return None return None @@ -71,6 +78,36 @@ def prompt_yes_no(prompt, default="Y"): return val.upper().startswith("Y") +def validate_hostname(hostname): + """Validate hostname format (basic check).""" + if not hostname or len(hostname) > 253: + return False + # Basic hostname regex: alphanumeric, hyphens, dots + pattern = r'^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' + return bool(re.match(pattern, hostname)) + + +def validate_targets(targets): + """Validate comma-separated ping targets.""" + import ipaddress + if not targets: + return False + target_list = [t.strip() for t in targets.split(",")] + if not target_list: + return False + for target in target_list: + # Try to parse as IP address first + try: + ipaddress.ip_address(target) + continue + except ValueError: + pass + # Otherwise validate as hostname + if not validate_hostname(target): + return False + return True + + def interactive_setup(saved_cfg=None): """Run interactive configuration and return config dict.""" print() @@ -97,24 +134,77 @@ def interactive_setup(saved_cfg=None): return cfg cfg = dict(DEFAULTS) - poll = prompt_default(" Poll interval (seconds)", cfg["poll"]) - cfg["poll"] = int(poll) - - threshold = prompt_default(" Failure threshold for drop", cfg["threshold"]) - cfg["threshold"] = int(threshold) - - targets = prompt_default(" Ping targets (comma-sep)", cfg["targets"]) - cfg["targets"] = targets - lat_warn = prompt_default(" Latency warning (ms)", cfg["lat_warn"]) - cfg["lat_warn"] = int(lat_warn) + # Poll interval validation + while True: + try: + poll = prompt_default(" Poll interval (seconds)", cfg["poll"]) + poll_val = float(poll) + if 0.1 <= poll_val <= 3600: + cfg["poll"] = poll_val + break + else: + print(" Error: Poll interval must be between 0.1 and 3600 seconds") + except ValueError: + print(" Error: Please enter a valid number") + + # Failure threshold validation + while True: + try: + threshold = prompt_default(" Failure threshold for drop", cfg["threshold"]) + threshold_val = int(threshold) + if 1 <= threshold_val <= 100: + cfg["threshold"] = threshold_val + break + else: + print(" Error: Threshold must be between 1 and 100") + except ValueError: + print(" Error: Please enter a valid integer") + + # Ping targets validation + while True: + targets = prompt_default(" Ping targets (comma-sep)", cfg["targets"]) + if validate_targets(targets): + cfg["targets"] = targets + break + else: + print(" Error: Invalid target format. Use IP addresses or hostnames (comma-separated)") + + # Latency warning validation + while True: + try: + lat_warn = prompt_default(" Latency warning (ms)", cfg["lat_warn"]) + lat_val = int(lat_warn) + if 1 <= lat_val <= 10000: + cfg["lat_warn"] = lat_val + break + else: + print(" Error: Latency warning must be between 1 and 10000 ms") + except ValueError: + print(" Error: Please enter a valid integer") cfg["enable_dns"] = prompt_yes_no(" Enable DNS health check?", "Y") if cfg["enable_dns"]: - cfg["dns_target"] = prompt_default(" DNS test hostname", cfg["dns_target"]) - - web_port = prompt_default(" Web dashboard port", cfg["web_port"]) - cfg["web_port"] = int(web_port) + while True: + dns_target = prompt_default(" DNS test hostname", cfg["dns_target"]) + if validate_hostname(dns_target): + cfg["dns_target"] = dns_target + break + else: + print(" Error: Invalid hostname format") + + # Web port validation + while True: + try: + web_port = prompt_default(" Web dashboard port", cfg["web_port"]) + port_val = int(web_port) + if 1 <= port_val <= 65535: + cfg["web_port"] = port_val + break + else: + print(" Error: Port must be between 1 and 65535") + except ValueError: + print(" Error: Please enter a valid port number") save_config(cfg) print(" Config saved to {}".format(get_config_path())) diff --git a/python/connectivity_monitor/metrics.py b/python/connectivity_monitor/metrics.py index 0ba42ca..f108d28 100644 --- a/python/connectivity_monitor/metrics.py +++ b/python/connectivity_monitor/metrics.py @@ -1,15 +1,42 @@ -"""Metrics engine — loss, avg, min, max, percentile, jitter, health, trend.""" +"""Metrics engine — loss, avg, min, max, percentile, jitter, health, trend. + +This module provides statistical analysis of network performance data. +All functions operate on a MonitorState object which contains ping history. + +Key Functions: +- loss(): Calculate packet loss percentage +- avg(), min_lat(), max_lat(): Latency statistics +- percentile(): Calculate percentile latency (e.g., p95, p99) +- jitter(): Average latency variation between consecutive pings +- uptime(): Percentage of successful pings +- get_health_score(): Overall connection health (0-100) with letter grade +- get_trend(): Trend analysis comparing recent vs baseline performance +""" import math def get_latency_values(state): - """Extract non-None latency values from history.""" + """Extract non-None latency values from history. + + Args: + state: MonitorState object with ping history + + Returns: + List of float latency values (excludes failed pings) + """ return [h["latency"] for h in state.history if h.get("latency") is not None] def loss(state): - """Packet loss percentage.""" + """Calculate packet loss percentage. + + Args: + state: MonitorState object + + Returns: + float: Packet loss percentage (0-100), rounded to 1 decimal place + """ total = len(state.history) if total == 0: return 0 @@ -18,7 +45,14 @@ def loss(state): def avg(state): - """Average latency of successful pings.""" + """Calculate average latency of successful pings. + + Args: + state: MonitorState object + + Returns: + float: Average latency in milliseconds, rounded to 1 decimal place + """ vals = get_latency_values(state) if not vals: return 0 @@ -26,7 +60,14 @@ def avg(state): def min_lat(state): - """Minimum latency.""" + """Calculate minimum latency. + + Args: + state: MonitorState object + + Returns: + float: Minimum latency in milliseconds, rounded to 1 decimal place + """ vals = get_latency_values(state) if not vals: return 0 @@ -34,7 +75,14 @@ def min_lat(state): def max_lat(state): - """Maximum latency.""" + """Calculate maximum latency. + + Args: + state: MonitorState object + + Returns: + float: Maximum latency in milliseconds, rounded to 1 decimal place + """ vals = get_latency_values(state) if not vals: return 0 @@ -42,7 +90,15 @@ def max_lat(state): def percentile(state, pct): - """Percentile latency (0-100).""" + """Calculate percentile latency (e.g., 95th percentile). + + Args: + state: MonitorState object + pct: Percentile value (0-100) + + Returns: + float: Latency at the specified percentile, rounded to 1 decimal place + """ vals = sorted(get_latency_values(state)) if not vals: return 0 @@ -53,7 +109,17 @@ def percentile(state, pct): def jitter(state): - """Average latency variation between consecutive pings.""" + """Calculate average latency variation between consecutive pings. + + Jitter is the average absolute difference between consecutive latency measurements. + High jitter indicates unstable network performance. + + Args: + state: MonitorState object + + Returns: + float: Average jitter in milliseconds, rounded to 1 decimal place + """ vals = get_latency_values(state) if len(vals) < 2: return 0 @@ -62,7 +128,14 @@ def jitter(state): def uptime(state): - """Percentage of successful pings.""" + """Calculate percentage of successful pings. + + Args: + state: MonitorState object + + Returns: + float: Uptime percentage (0-100), rounded to 2 decimal places + """ total = len(state.history) if total == 0: return 100.0 @@ -71,7 +144,29 @@ def uptime(state): def get_health_score(state): - """Health score 0-100 with letter grade. Returns {score, grade, color}.""" + """Calculate overall connection health score with letter grade. + + Health score formula: + - Start at 100 + - Subtract 3 points per 1% packet loss + - Subtract points for high average latency (scaled) + - Subtract points for high jitter (scaled) + + Grading: + - A+: 90-100 (excellent) + - A: 80-89 (great) + - B: 70-79 (good) + - C: 60-69 (fair) + - D: 50-59 (poor) + - F: 0-49 (failing) + + Args: + state: MonitorState object + + Returns: + dict: {'score': int (0-100), 'grade': str, 'color': str} + where color is suitable for terminal/web display + """ l = loss(state) a = avg(state) j = jitter(state) diff --git a/python/connectivity_monitor/network.py b/python/connectivity_monitor/network.py index 2395455..196b0e2 100644 --- a/python/connectivity_monitor/network.py +++ b/python/connectivity_monitor/network.py @@ -40,7 +40,8 @@ def ping_test(target, timeout=2): # Got response but couldn't parse latency — still ok return {"ok": True, "lat": 0, "target": target, "time": ts} return {"ok": False, "lat": None, "target": target, "time": ts} - except Exception: + except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError, ValueError) as e: + # Network errors, command failures, or parsing errors return {"ok": False, "lat": None, "target": target, "time": ts} @@ -51,7 +52,8 @@ def dns_test(hostname): socket.getaddrinfo(hostname, None) elapsed = (time.monotonic() - start) * 1000 return {"ok": True, "ms": round(elapsed, 1)} - except Exception: + except (socket.gaierror, socket.timeout, OSError) as e: + # DNS resolution failures return {"ok": False, "ms": None} @@ -82,7 +84,8 @@ def detect_gateway(): m = re.search(r"Default Gateway.*?:\s*([\d.]+)", out.stdout) if m: return m.group(1) - except Exception: + except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError) as e: + # Command execution failures pass return "N/A" @@ -90,12 +93,13 @@ def detect_gateway(): def get_local_ip(): """Get local IP address of the machine.""" try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) - ip = s.getsockname()[0] - s.close() - return ip - except Exception: + # Use context manager for automatic socket cleanup + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + return ip + except (socket.error, OSError) as e: + # Network/socket errors return "N/A" @@ -112,7 +116,8 @@ def detect_public_ip(state): data = json_mod.loads(resp.read().decode()) state.public_ip = data.get("query", "N/A") state.isp_name = data.get("isp", "N/A") - except Exception: + except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError) as e: + # Network errors or JSON parsing failures state.public_ip = "N/A" state.isp_name = "N/A" diff --git a/python/connectivity_monitor/web_server.py b/python/connectivity_monitor/web_server.py index 55b1822..e25da1d 100644 --- a/python/connectivity_monitor/web_server.py +++ b/python/connectivity_monitor/web_server.py @@ -1,6 +1,7 @@ """Built-in HTTP web server — live dashboard, JSON API, file serving.""" import datetime +import html import json import os import threading @@ -154,6 +155,14 @@ def _serve_file(self, base_dir, filename, content_type): # Sanitize filename to prevent path traversal safe_name = os.path.basename(filename) filepath = os.path.join(base_dir, safe_name) + + # Verify the resolved path is still within the base directory + real_base = os.path.realpath(base_dir) + real_file = os.path.realpath(filepath) + if not real_file.startswith(real_base + os.sep): + self._send(403, "text/plain", "Forbidden") + return + if os.path.isfile(filepath): with open(filepath, "rb") as f: self._send(200, content_type, f.read()) @@ -167,13 +176,15 @@ def _serve_file_list(self, directory, title, url_prefix): files = sorted(os.listdir(directory), reverse=True) items = "" for f in files: + # HTML-escape filenames to prevent XSS + safe_f = html.escape(f) items += '