security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324) - Add usedforsecurity=False to MD5 hash in gateway_watcher.py - Add URL scheme validation to prevent file:// access in config.py - Add URL validation to bootstrap.py health check - Add nosec comments where runtime validation exists * fix: handle ConnectionResetError gracefully and add debug logging - Add QuietHTTPServer class to suppress noisy connection reset errors caused by clients disconnecting abruptly (fixes log spam from 'ConnectionResetError: [Errno 54] Connection reset by peer') - Replace silent 'pass' statements with logger.debug() calls across api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py, and api/onboarding.py for better observability during troubleshooting - All tests pass (25 passed in test_regressions.py) * chore: add debug logging to profiles and routes modules - Replace silent 'pass' statements with logger.debug() calls in api/profiles.py for better error visibility during profile switching and module patching - Add logger initialization to api/routes.py * security: fix B110 bare except/pass issues (bandit security scan) - Replace bare except/pass patterns with logger.debug() calls - Fixes CWE-703 (improper check/handling of exceptional conditions) - Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py - All tests pass successfully * security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) - api/gateway_watcher.py: MD5 usedforsecurity=False (B324) - api/config.py, bootstrap.py: URL scheme validation before urlopen (B310) - 12 files: replace bare except/pass with logger.debug() (B110) - server.py: QuietHTTPServer suppresses client disconnect log noise - server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail) - tests/test_sprint43.py: 19 new tests covering all security fixes - CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822) --------- Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@ Discovery order for all paths:
|
||||
|
||||
import collections
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
@@ -48,6 +49,8 @@ SETTINGS_FILE = STATE_DIR / "settings.json"
|
||||
LAST_WORKSPACE_FILE = STATE_DIR / "last_workspace.txt"
|
||||
PROJECTS_FILE = STATE_DIR / "projects.json"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Hermes agent directory discovery ─────────────────────────────────────────
|
||||
def _discover_agent_dir() -> Path:
|
||||
@@ -197,7 +200,7 @@ def reload_config() -> None:
|
||||
if isinstance(loaded, dict):
|
||||
_cfg_cache.update(loaded)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to load yaml config from %s", config_path)
|
||||
|
||||
|
||||
# Initial load
|
||||
@@ -601,7 +604,7 @@ def get_available_models() -> dict:
|
||||
auth_store = _j.loads(auth_store_path.read_text())
|
||||
active_provider = auth_store.get("active_provider")
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to load auth store from %s", auth_store_path)
|
||||
|
||||
# 4. Detect available providers.
|
||||
# Primary: ask hermes-agent's auth layer — the authoritative source. It checks
|
||||
@@ -629,11 +632,11 @@ def get_available_models() -> dict:
|
||||
if _src == "gh auth token":
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to get key source for provider %s", _p.get("id", "unknown"))
|
||||
detected_providers.add(_p["id"])
|
||||
_hermes_auth_used = True
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to detect auth providers from hermes")
|
||||
|
||||
if not _hermes_auth_used:
|
||||
# Fallback: scan .env and os.environ for known API key variables
|
||||
@@ -652,7 +655,7 @@ def get_available_models() -> dict:
|
||||
k, v = line.split("=", 1)
|
||||
env_keys[k.strip()] = v.strip().strip('"').strip("'")
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to parse hermes env file")
|
||||
all_env = {**env_keys}
|
||||
for k in (
|
||||
"ANTHROPIC_API_KEY",
|
||||
@@ -760,6 +763,9 @@ def get_available_models() -> dict:
|
||||
parsed_url = urlparse(
|
||||
endpoint_url if "://" in endpoint_url else f"http://{endpoint_url}"
|
||||
)
|
||||
# Validate URL scheme to prevent file:// and other dangerous schemes
|
||||
if parsed_url.scheme not in ("", "http", "https"):
|
||||
raise ValueError(f"Invalid URL scheme: {parsed_url.scheme}")
|
||||
if parsed_url.hostname:
|
||||
try:
|
||||
resolved_ips = socket.getaddrinfo(parsed_url.hostname, None)
|
||||
@@ -791,7 +797,7 @@ def get_available_models() -> dict:
|
||||
req.add_header("User-Agent", "OpenAI/Python 1.0")
|
||||
for k, v in headers.items():
|
||||
req.add_header(k, v)
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
with urllib.request.urlopen(req, timeout=10) as response: # nosec B310
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
# Handle both OpenAI-compatible and llama.cpp response formats
|
||||
@@ -814,7 +820,7 @@ def get_available_models() -> dict:
|
||||
auto_detected_models.append({"id": model_id, "label": model_name})
|
||||
detected_providers.add(provider.lower())
|
||||
except Exception:
|
||||
pass # custom endpoint unreachable or misconfigured -- fail silently
|
||||
logger.debug("Custom endpoint unreachable or misconfigured for provider: %s", provider)
|
||||
|
||||
# 3b. Include models from custom_providers config entries.
|
||||
# These are explicitly configured and should always appear even when the
|
||||
@@ -1026,7 +1032,7 @@ def load_settings() -> dict:
|
||||
if isinstance(stored, dict):
|
||||
settings.update(stored)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to load settings from %s", SETTINGS_FILE)
|
||||
return settings
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user