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:
@@ -9,12 +9,15 @@ cached paths in hermes-agent modules (skills_tool, cron/jobs) that snapshot
|
||||
HERMES_HOME at import time.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Constants (match hermes_cli.profiles upstream) ─────────────────────────
|
||||
_PROFILE_ID_RE = re.compile(r'^[a-z0-9][a-z0-9_-]{0,63}$')
|
||||
_PROFILE_DIRS = [
|
||||
@@ -76,7 +79,7 @@ def _read_active_profile_file() -> str:
|
||||
if name:
|
||||
return name
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to read active profile file")
|
||||
return 'default'
|
||||
|
||||
|
||||
@@ -107,7 +110,7 @@ def _set_hermes_home(home: Path):
|
||||
_sk.HERMES_HOME = home
|
||||
_sk.SKILLS_DIR = home / 'skills'
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
logger.debug("Failed to patch skills_tool module")
|
||||
|
||||
# Patch cron/jobs module-level cache
|
||||
try:
|
||||
@@ -117,7 +120,7 @@ def _set_hermes_home(home: Path):
|
||||
_cj.JOBS_FILE = _cj.CRON_DIR / 'jobs.json'
|
||||
_cj.OUTPUT_DIR = _cj.CRON_DIR / 'output'
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
logger.debug("Failed to patch cron.jobs module")
|
||||
|
||||
|
||||
def _reload_dotenv(home: Path):
|
||||
@@ -151,6 +154,7 @@ def _reload_dotenv(home: Path):
|
||||
_loaded_profile_env_keys = loaded_keys
|
||||
except Exception:
|
||||
_loaded_profile_env_keys = set()
|
||||
logger.debug("Failed to reload dotenv from %s", env_path)
|
||||
|
||||
|
||||
def init_profile_state() -> None:
|
||||
@@ -206,7 +210,7 @@ def switch_profile(name: str) -> dict:
|
||||
ap_file = _DEFAULT_HERMES_HOME / 'active_profile'
|
||||
ap_file.write_text(name if name != 'default' else '')
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to write active profile file")
|
||||
|
||||
# Reload config.yaml from the new profile
|
||||
reload_config()
|
||||
@@ -326,7 +330,7 @@ def _write_endpoint_to_config(profile_dir: Path, base_url: str = None, api_key:
|
||||
if isinstance(loaded, dict):
|
||||
cfg = loaded
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to load config from %s", config_path)
|
||||
model_section = cfg.get('model', {})
|
||||
if not isinstance(model_section, dict):
|
||||
model_section = {}
|
||||
@@ -371,7 +375,7 @@ def create_profile_api(name: str, clone_from: str = None,
|
||||
try:
|
||||
profile_path = Path(p.get('path') or profile_path)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Failed to parse profile path")
|
||||
break
|
||||
|
||||
profile_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user