security: harden exception exposure, SSL config, and Pulse response parsing
Lint / Python (flake8) (push) Failing after 42s
Lint / JS (eslint) (push) Successful in 7s
Security / Python Security (bandit) (push) Successful in 1m22s
Test / Python Tests (pytest) (push) Failing after 1m23s
Lint / Notify on failure (push) Successful in 3s
Lint / Deploy (push) Has been skipped
Lint / Python (flake8) (push) Failing after 42s
Lint / JS (eslint) (push) Successful in 7s
Security / Python Security (bandit) (push) Successful in 1m22s
Test / Python Tests (pytest) (push) Failing after 1m23s
Lint / Notify on failure (push) Successful in 3s
Lint / Deploy (push) Has been skipped
- app.py: replace raw str(e) in diagnostic _run() with generic client message; log internally only - app.py: /health endpoint no longer leaks exception strings to unauthenticated callers; errors logged server-side - monitor.py: UniFi SSL verification now defaults True, configurable via config.json unifi.verify_ssl; urllib3 warning suppression scoped to verify=False only (removed global disable) - monitor.py: Pulse execution_id extracted with .get() + explicit None check to avoid KeyError on malformed response - monitor.py: interface name regex drops '@' (not a valid kernel interface char) to match app.py and fix inconsistency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -451,7 +451,7 @@ def api_diagnose_start():
|
|||||||
result = runner.run(host_ip, server_name, matched_iface, port_data)
|
result = runner.run(host_ip, server_name, matched_iface, port_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Diagnostic job {job_id} failed: {e}', exc_info=True)
|
logger.error(f'Diagnostic job {job_id} failed: {e}', exc_info=True)
|
||||||
result = {'status': 'error', 'error': str(e)}
|
result = {'status': 'error', 'error': 'Diagnostic failed; check server logs.'}
|
||||||
with _diag_lock:
|
with _diag_lock:
|
||||||
if job_id in _diag_jobs:
|
if job_id in _diag_jobs:
|
||||||
_diag_jobs[job_id]['status'] = 'done'
|
_diag_jobs[job_id]['status'] = 'done'
|
||||||
@@ -563,7 +563,8 @@ def health():
|
|||||||
db.get_state('last_check')
|
db.get_state('last_check')
|
||||||
checks['db'] = 'ok'
|
checks['db'] = 'ok'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
checks['db'] = f'error: {e}'
|
logger.error(f'Health check db error: {e}')
|
||||||
|
checks['db'] = 'error'
|
||||||
overall = 'degraded'
|
overall = 'degraded'
|
||||||
|
|
||||||
# Monitor freshness: fail if last_check is older than 20 minutes
|
# Monitor freshness: fail if last_check is older than 20 minutes
|
||||||
@@ -580,7 +581,8 @@ def health():
|
|||||||
else:
|
else:
|
||||||
checks['monitor'] = 'no data yet'
|
checks['monitor'] = 'no data yet'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
checks['monitor'] = f'error: {e}'
|
logger.error(f'Health check monitor error: {e}')
|
||||||
|
checks['monitor'] = 'error'
|
||||||
overall = 'degraded'
|
overall = 'degraded'
|
||||||
|
|
||||||
status_code = 200 if overall == 'ok' else 503
|
status_code = 200 if overall == 'ok' else 503
|
||||||
|
|||||||
+8
-4
@@ -20,7 +20,6 @@ from urllib3.exceptions import InsecureRequestWarning
|
|||||||
|
|
||||||
import db
|
import db
|
||||||
|
|
||||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -90,7 +89,9 @@ class UnifiClient:
|
|||||||
self.base_url = cfg['controller']
|
self.base_url = cfg['controller']
|
||||||
self.site_id = cfg.get('site_id', 'default')
|
self.site_id = cfg.get('site_id', 'default')
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.session.verify = False
|
self.session.verify = cfg.get('verify_ssl', True)
|
||||||
|
if not self.session.verify:
|
||||||
|
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||||
self.headers = {
|
self.headers = {
|
||||||
'X-API-KEY': cfg['api_key'],
|
'X-API-KEY': cfg['api_key'],
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@@ -262,7 +263,10 @@ class PulseClient:
|
|||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
execution_id = resp.json()['execution_id']
|
execution_id = resp.json().get('execution_id')
|
||||||
|
if not execution_id:
|
||||||
|
logger.error('Pulse submit response missing execution_id')
|
||||||
|
return None
|
||||||
self.last_execution_id = execution_id
|
self.last_execution_id = execution_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f'Pulse command submit failed: {e}')
|
logger.error(f'Pulse command submit failed: {e}')
|
||||||
@@ -352,7 +356,7 @@ class LinkStatsCollector:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Validate interface names (kernel names only contain [a-zA-Z0-9_.-])
|
# Validate interface names (kernel names only contain [a-zA-Z0-9_.-])
|
||||||
safe_ifaces = [i for i in ifaces if re.match(r'^[a-zA-Z0-9_.@-]+$', i)]
|
safe_ifaces = [i for i in ifaces if re.match(r'^[a-zA-Z0-9_.-]+$', i)]
|
||||||
if not safe_ifaces:
|
if not safe_ifaces:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user