diff --git a/bridge/dashboard_api.py b/bridge/dashboard_api.py index a608123a8..3b89190c7 100644 --- a/bridge/dashboard_api.py +++ b/bridge/dashboard_api.py @@ -14,6 +14,7 @@ import os import json +import logging import time from flask import Blueprint, jsonify, request @@ -33,6 +34,7 @@ # Cache configuration (in-memory for simplicity) CACHE_TTL = 30 # seconds _price_cache = {"data": None, "timestamp": 0} +logger = logging.getLogger(__name__) # ─── Blueprint ──────────────────────────────────────────────────────────────── dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/bridge/dashboard") @@ -152,8 +154,9 @@ def get_bridge_health(): conn.execute("SELECT 1").fetchone() health["rustchain"] = True details["rustchain"] = "Database accessible" - except Exception as e: - details["rustchain"] = f"Database error: {str(e)}" + except Exception: + logger.exception("Dashboard health database check failed") + details["rustchain"] = "Database unavailable" # Check Solana RPC (sync version) try: @@ -173,8 +176,9 @@ def get_bridge_health(): details["solana_rpc"] = "RPC responsive" else: details["solana_rpc"] = "RPC returned unexpected response" - except Exception as e: - details["solana_rpc"] = f"RPC error: {str(e)}" + except Exception: + logger.exception("Dashboard health Solana RPC check failed") + details["solana_rpc"] = "RPC unavailable" # Bridge API is healthy if we got here health["bridge_api"] = True @@ -200,8 +204,9 @@ def get_bridge_health(): details["wrtc_mint"] = "Mint account exists" else: details["wrtc_mint"] = "Mint account not found" - except Exception as e: - details["wrtc_mint"] = f"Mint check error: {str(e)}" + except Exception: + logger.exception("Dashboard health wRTC mint check failed") + details["wrtc_mint"] = "Mint check unavailable" else: health["wrtc_mint"] = True # Skip if not configured details["wrtc_mint"] = "Mint address not configured" diff --git a/bridge/test_dashboard_api.py b/bridge/test_dashboard_api.py index faa024d9c..f3011f94a 100644 --- a/bridge/test_dashboard_api.py +++ b/bridge/test_dashboard_api.py @@ -17,12 +17,14 @@ import time import sys import os +import urllib.request # Add parent directory to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from flask import Flask from bridge.bridge_api import register_bridge_routes, init_bridge_db, get_db, _amount_to_base, STATE_COMPLETE +import bridge.dashboard_api as dashboard_api from bridge.dashboard_api import register_dashboard_routes @@ -186,6 +188,66 @@ def test_health_timestamp(self, client): now = int(time.time()) assert abs(data['last_checked'] - now) < 5 # Within 5 seconds + def test_health_hides_database_exception_details(self, client, monkeypatch): + """Health checks should not expose local DB paths or exception text.""" + secret_error = "sqlite failure at C:/srv/rustchain/private/bridge.db" + + class FailingDb: + def __enter__(self): + raise RuntimeError(secret_error) + + def __exit__(self, *_args): + return False + + def fail_rpc(*_args, **_kwargs): + raise RuntimeError("rpc unavailable") + + monkeypatch.setattr(dashboard_api, "get_db", lambda: FailingDb()) + monkeypatch.setattr(urllib.request, "urlopen", fail_rpc) + + response = client.get('/bridge/dashboard/health') + + assert response.status_code == 200 + body = json.loads(response.data) + assert body["components"]["rustchain"] is False + assert body["details"]["rustchain"] == "Database unavailable" + assert secret_error not in response.get_data(as_text=True) + + def test_health_hides_rpc_exception_details(self, client, monkeypatch): + """Health checks should not expose RPC URLs or network exception text.""" + secret_error = "GET https://internal-solana.local/rpc?token=secret failed" + + def fail_rpc(*_args, **_kwargs): + raise RuntimeError(secret_error) + + monkeypatch.setattr(urllib.request, "urlopen", fail_rpc) + + response = client.get('/bridge/dashboard/health') + + assert response.status_code == 200 + body = json.loads(response.data) + assert body["components"]["solana_rpc"] is False + assert body["details"]["solana_rpc"] == "RPC unavailable" + assert secret_error not in response.get_data(as_text=True) + + def test_health_hides_mint_exception_details(self, client, monkeypatch): + """Health checks should not expose configured mint lookup errors.""" + secret_error = "mint lookup failed for account 7xPrivateInternalMint" + + def fail_rpc(*_args, **_kwargs): + raise RuntimeError(secret_error) + + monkeypatch.setattr(dashboard_api, "WRTC_MINT_ADDRESS", "7xPrivateInternalMint") + monkeypatch.setattr(urllib.request, "urlopen", fail_rpc) + + response = client.get('/bridge/dashboard/health') + + assert response.status_code == 200 + body = json.loads(response.data) + assert body["components"]["wrtc_mint"] is False + assert body["details"]["wrtc_mint"] == "Mint check unavailable" + assert secret_error not in response.get_data(as_text=True) + class TestDashboardTransactions: """Test /bridge/dashboard/transactions endpoint."""