From 71327371c33222dd3cc5e88d0fb14fc301d41488 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:25:51 +0000 Subject: [PATCH] Audit fixes: security, bugs, code quality improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix HTTP → HTTPS for ip-api.com in macOS and Windows to prevent man-in-the-middle interception of public IP/ISP lookups - Fix falsy-check bug in headless_config: use `is not None` for poll and threshold so explicit value 0 is honoured - Add input validation in interactive_setup: replace bare int() casts with _prompt_int() which re-prompts on invalid input - Add HTML escaping (html.escape) for ISP name, public IP, target names, and drop data injected into HTML reports - Move import datetime from inside functions to module level in metrics.py - Use try/finally for socket cleanup in network.py get_local_ip - Rename single-letter variables (l, a, j) in get_health_score to loss_pct, avg_lat, jitter_ms for readability - Fix confusingly-named variables now_s/end_s → start_s/end_s in macos/lib/logging.sh log_drop and log_threshold_breach Agent-Logs-Url: https://github.com/nexuspcs/ConnectivityMonitor/sessions/36342ce1-c124-412a-9d81-904bbde9071a Co-authored-by: nexuspcs <69493073+nexuspcs@users.noreply.github.com> --- macos/lib/logging.sh | 16 +++++------ macos/lib/network.sh | 2 +- python/connectivity_monitor/config.py | 32 +++++++++++++--------- python/connectivity_monitor/html_report.py | 22 +++++++++++---- python/connectivity_monitor/metrics.py | 24 ++++++++-------- python/connectivity_monitor/network.py | 9 +++--- windows/ConnectivityDropMonitor.ps1 | 2 +- 7 files changed, 62 insertions(+), 45 deletions(-) diff --git a/macos/lib/logging.sh b/macos/lib/logging.sh index 4abb92c..ef5411d 100755 --- a/macos/lib/logging.sh +++ b/macos/lib/logging.sh @@ -47,10 +47,10 @@ log_ping() { log_drop() { local start="$1" end="$2" target="$3" diagnosis="$4" - local now_s end_s dur - now_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) - end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) - dur=$(awk "BEGIN {printf \"%.2f\", $now_s - $end_s}") + local start_s end_s dur + start_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) + end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) + dur=$(awk "BEGIN {printf \"%.2f\", $end_s - $start_s}") if awk "BEGIN {exit !($dur < 0)}"; then dur="0"; fi drop_starts+=("$start") @@ -67,10 +67,10 @@ log_drop() { log_threshold_breach() { local start="$1" end="$2" avg_lat="$3" - local now_s end_s dur - now_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) - end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) - dur=$(awk "BEGIN {printf \"%.2f\", $now_s - $end_s}") + local start_s end_s dur + start_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) + end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) + dur=$(awk "BEGIN {printf \"%.2f\", $end_s - $start_s}") if awk "BEGIN {exit !($dur < 0)}"; then dur="0"; fi breach_starts+=("$start") 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/python/connectivity_monitor/config.py b/python/connectivity_monitor/config.py index c6b5aed..54a9cbc 100644 --- a/python/connectivity_monitor/config.py +++ b/python/connectivity_monitor/config.py @@ -58,19 +58,30 @@ def save_config(cfg): def prompt_default(prompt, default): - """Prompt user with a default value.""" + """Prompt user with a default value. Returns the user's input or the default.""" val = input("{} [{}]: ".format(prompt, default)).strip() return val if val else str(default) def prompt_yes_no(prompt, default="Y"): - """Prompt user for yes/no.""" + """Prompt user for yes/no. Returns True for yes, False for no.""" val = input("{} [{}]: ".format(prompt, default)).strip() if not val: val = default return val.upper().startswith("Y") +def _prompt_int(prompt, default): + """Prompt user for an integer, re-prompting on invalid input.""" + while True: + raw = input("{} [{}]: ".format(prompt, default)).strip() + val = raw if raw else str(default) + try: + return int(val) + except ValueError: + print(" Invalid value '{}' — please enter a whole number.".format(val)) + + def interactive_setup(saved_cfg=None): """Run interactive configuration and return config dict.""" print() @@ -97,24 +108,19 @@ 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) + cfg["poll"] = _prompt_int(" Poll interval (seconds)", cfg["poll"]) + cfg["threshold"] = _prompt_int(" Failure threshold for drop", cfg["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) + cfg["lat_warn"] = _prompt_int(" Latency warning (ms)", cfg["lat_warn"]) 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) + cfg["web_port"] = _prompt_int(" Web dashboard port", cfg["web_port"]) save_config(cfg) print(" Config saved to {}".format(get_config_path())) @@ -130,9 +136,9 @@ def headless_config(args): if args.targets: cfg["targets"] = args.targets - if args.poll: + if args.poll is not None: cfg["poll"] = args.poll - if args.threshold: + if args.threshold is not None: cfg["threshold"] = args.threshold if args.web_port is not None: cfg["web_port"] = args.web_port diff --git a/python/connectivity_monitor/html_report.py b/python/connectivity_monitor/html_report.py index ecdebad..251dc50 100644 --- a/python/connectivity_monitor/html_report.py +++ b/python/connectivity_monitor/html_report.py @@ -4,6 +4,7 @@ """ import datetime +import html import os from . import metrics @@ -141,7 +142,11 @@ def generate_html_report(state, report_file): report_gen_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") total_lost = state.total_pings - state.total_success - html = _build_html( + # Escape external/user-supplied strings before injecting into HTML + safe_isp = html.escape(str(state.isp_name)) + safe_public_ip = html.escape(str(state.public_ip)) + + html_out = _build_html( labels_js=labels_js, data_js=data_js, gw_js=gw_js, hist_js=hist_js, target_rows=target_rows, drops_table=drops_table, breach_table=breach_table, heatmap_html=heatmap_html, report_date=report_date, @@ -152,14 +157,14 @@ def generate_html_report(state, report_file): total_downtime=total_downtime, baseline_text=baseline_text, health_css=health_css, uptime_css=uptime_css, avg_css=avg_css, loss_css=loss_css, jitter_css=jitter_css, drops_css=drops_css, - drops_count=len(state.drops), isp=state.isp_name, public_ip=state.public_ip, + drops_count=len(state.drops), isp=safe_isp, public_ip=safe_public_ip, ) d = os.path.dirname(report_file) if d: os.makedirs(d, exist_ok=True) with open(report_file, "w", encoding="utf-8") as f: - f.write(html) + f.write(html_out) def _score_color(value, thresholds, default): @@ -190,7 +195,7 @@ def _build_target_rows(state): rows += ( "