diff --git a/GrumpyCats/litterbox_client/system.py b/GrumpyCats/litterbox_client/system.py index df7848e..8385bcc 100644 --- a/GrumpyCats/litterbox_client/system.py +++ b/GrumpyCats/litterbox_client/system.py @@ -55,9 +55,9 @@ class SystemMixin: def get_scanners_status(self) -> Dict: """Inventory of configured analyzers and whether their binaries - exist on disk (drives the dashboard scanner panel).""" - response = self._make_request("GET", "/api/system/scanners") - return response.json() + exist on disk. Returns the `scanners` field of the unified /health + response: `{rows: [...], counts: {...}}`.""" + return (self.check_health() or {}).get("scanners", {"rows": [], "counts": {}}) # ---- destructive --------------------------------------------------- diff --git a/app/blueprints/api.py b/app/blueprints/api.py index 4017a61..de6ac18 100644 --- a/app/blueprints/api.py +++ b/app/blueprints/api.py @@ -111,53 +111,6 @@ def api_edr_profiles(): return jsonify({'profiles': deps.edr_registry.list_profiles()}) -@api_bp.route('/api/system/scanners', methods=['GET']) -@error_handler -def api_system_scanners(): - """Inventory of configured analyzers and whether their binaries exist. - - Walks the static + dynamic + holygrail sections of analysis config and - reports per-scanner: enabled flag, configured tool path, whether the - file is present on disk. Used by the dashboard to flag missing tools.""" - cfg = current_app.config.get('analysis', {}) or {} - - def _row(group, name, scanner_cfg): - tool_path = (scanner_cfg or {}).get('tool_path', '').strip() - enabled = bool((scanner_cfg or {}).get('enabled', False)) - exists = bool(tool_path) and os.path.isfile(tool_path) - return { - 'group': group, - 'name': name, - 'enabled': enabled, - 'tool_path': tool_path, - 'exists': exists, - 'status': ( - 'ok' if enabled and exists else - 'missing' if enabled and not exists else - 'disabled' - ), - } - - rows = [] - for group_key in ('static', 'dynamic'): - group_cfg = cfg.get(group_key) or {} - for scanner_name, scanner_cfg in group_cfg.items(): - if isinstance(scanner_cfg, dict): - rows.append(_row(group_key, scanner_name, scanner_cfg)) - - holygrail = cfg.get('holygrail') - if isinstance(holygrail, dict): - rows.append(_row('holygrail', 'holygrail', holygrail)) - - counts = { - 'total': len(rows), - 'ok': sum(1 for r in rows if r['status'] == 'ok'), - 'missing': sum(1 for r in rows if r['status'] == 'missing'), - 'disabled': sum(1 for r in rows if r['status'] == 'disabled'), - } - return jsonify({'scanners': rows, 'counts': counts}) - - @api_bp.route('/api/edr/fibratus//alerts/since', methods=['GET']) @error_handler def api_fibratus_alerts_passthrough(profile): diff --git a/app/blueprints/management.py b/app/blueprints/management.py index f4c3a18..26fd8bf 100644 --- a/app/blueprints/management.py +++ b/app/blueprints/management.py @@ -8,7 +8,11 @@ from datetime import datetime from flask import Blueprint, current_app, jsonify from ..services.error_handling import error_handler -from ..services.tool_check import check_analysis_tool, check_holygrail_tool +from ..services.tool_check import ( + check_analysis_tool, + check_holygrail_tool, + scanner_inventory, +) from ..utils import path_manager management_bp = Blueprint('management', __name__) @@ -100,6 +104,16 @@ def cleanup(): @management_bp.route('/health', methods=['GET']) @error_handler def health_check(): + """Unified sandbox health: boot validation + scanner inventory + EDR agents. + + Replaces the previous narrow scanner-config-only response. Three sections: + - `sandbox` — upload folder + boot-config issues + - `scanners` — per-analyzer inventory (was `/api/system/scanners`) + - `edr_agents` — live agent + backend reachability + (delegates to the same TTL-cached probe `/api/edr/agents/status` uses) + """ + from ..services import edr_health + app = current_app app.logger.debug("Starting health check.") config = app.config @@ -127,15 +141,11 @@ def health_check(): check_holygrail_tool(holygrail_section, issues, app.logger) - static_tools = { - tool: static_section.get(tool, {}).get('enabled', False) - for tool in static_section.keys() - } - dynamic_tools = { - tool: dynamic_section.get(tool, {}).get('enabled', False) - for tool in dynamic_section.keys() - } - holygrail_status = holygrail_section.get('enabled', False) + scanner_rows, scanner_counts = scanner_inventory(analysis_config) + + deps = current_app.extensions['litterbox'] + profiles = list(deps.edr_registry._PROFILES.values()) + edr_snapshot = edr_health.get_status_snapshot(profiles) status = 'ok' if not issues else 'degraded' app.logger.debug(f"Health check completed. Status: {status}") @@ -143,13 +153,15 @@ def health_check(): return jsonify({ 'status': status, 'timestamp': datetime.now().isoformat(), - 'upload_folder_accessible': os.path.isdir(upload_folder) if upload_folder else False, 'issues': issues, - 'configuration': { - 'static_analysis': static_tools, - 'dynamic_analysis': dynamic_tools, - 'holygrail_analysis': holygrail_status, + 'sandbox': { + 'upload_folder_accessible': os.path.isdir(upload_folder) if upload_folder else False, }, + 'scanners': { + 'rows': scanner_rows, + 'counts': scanner_counts, + }, + 'edr_agents': edr_snapshot, }), 200 if status == 'ok' else 503 diff --git a/app/blueprints/upload.py b/app/blueprints/upload.py index 47d5b10..4735a2c 100644 --- a/app/blueprints/upload.py +++ b/app/blueprints/upload.py @@ -11,8 +11,8 @@ upload_bp = Blueprint('upload', __name__) @upload_bp.route('/') def index(): """System health dashboard: agents + scanner availability. - Live data is fetched async by the page's JS via /api/edr/agents/status - and /api/system/scanners.""" + Live data is fetched async by the page's JS via /health (single + request returns scanner inventory + EDR agent reachability).""" deps = current_app.extensions['litterbox'] return render_template( 'dashboard.html', diff --git a/app/services/tool_check.py b/app/services/tool_check.py index 27c23e1..a30eb09 100644 --- a/app/services/tool_check.py +++ b/app/services/tool_check.py @@ -1,8 +1,53 @@ # app/services/tool_check.py -"""Tool/path validation helpers for the /health endpoint.""" +"""Tool/path validation + inventory helpers for the /health endpoint.""" import os +def scanner_inventory(analysis_cfg): + """Per-scanner inventory: enabled flag, configured path, exists-on-disk. + + Walks the static + dynamic + holygrail sections of analysis config and + returns `(rows, counts)` for the unified /health response. Same shape as + the (now-removed) /api/system/scanners endpoint. + """ + def _row(group, name, scanner_cfg): + tool_path = (scanner_cfg or {}).get('tool_path', '').strip() + enabled = bool((scanner_cfg or {}).get('enabled', False)) + exists = bool(tool_path) and os.path.isfile(tool_path) + return { + 'group': group, + 'name': name, + 'enabled': enabled, + 'tool_path': tool_path, + 'exists': exists, + 'status': ( + 'ok' if enabled and exists else + 'missing' if enabled and not exists else + 'disabled' + ), + } + + cfg = analysis_cfg or {} + rows = [] + for group_key in ('static', 'dynamic'): + group_cfg = cfg.get(group_key) or {} + for scanner_name, scanner_cfg in group_cfg.items(): + if isinstance(scanner_cfg, dict): + rows.append(_row(group_key, scanner_name, scanner_cfg)) + + holygrail = cfg.get('holygrail') + if isinstance(holygrail, dict): + rows.append(_row('holygrail', 'holygrail', holygrail)) + + counts = { + 'total': len(rows), + 'ok': sum(1 for r in rows if r['status'] == 'ok'), + 'missing': sum(1 for r in rows if r['status'] == 'missing'), + 'disabled': sum(1 for r in rows if r['status'] == 'disabled'), + } + return rows, counts + + def check_analysis_tool(section, tool_name, issues, logger): """Append issues for a static or dynamic analysis tool.""" tool_config = section.get(tool_name, {}) diff --git a/app/static/js/dashboard/core.js b/app/static/js/dashboard/core.js index 8e5742a..590badc 100644 --- a/app/static/js/dashboard/core.js +++ b/app/static/js/dashboard/core.js @@ -1,9 +1,8 @@ // app/static/js/dashboard/core.js // -// Drives the index dashboard. Polls /api/system/scanners for analyzer -// availability and /api/edr/agents/status for live agent + Elastic state. -// Auto-refreshes every 60s; the manual Refresh button forces an immediate -// poll. Both fetches are kicked off in parallel. +// Drives the index dashboard. Single /health fetch returns scanner +// inventory + live EDR agent reachability in one shot. Auto-refreshes +// every 60s; the manual Refresh button forces an immediate poll. const REFRESH_MS = 60000; let _refreshTimer = null; @@ -27,7 +26,7 @@ function statusTagClass(status) { function renderScanners(payload) { const host = document.getElementById('scannersTable'); if (!host) return; - const scanners = (payload && payload.scanners) || []; + const scanners = (payload && payload.rows) || []; const counts = (payload && payload.counts) || {}; setText('scannersCount', `${counts.ok ?? 0} ok · ${counts.missing ?? 0} missing · ${counts.disabled ?? 0} disabled`); @@ -119,23 +118,20 @@ async function refreshDashboard() { if (btn) btn.disabled = true; try { - const [scannersResp, agentsResp] = await Promise.all([ - fetch('/api/system/scanners', { cache: 'no-store' }), - fetch('/api/edr/agents/status', { cache: 'no-store' }), - ]); - - if (scannersResp.ok) { - renderScanners(await scannersResp.json()); - } else { + // Single /health fetch returns both scanner inventory and EDR + // reachability. /health responds 200 (ok) or 503 (degraded) — both + // carry a usable payload, so we read JSON either way. + const resp = await fetch('/health', { cache: 'no-store' }); + if (resp.status !== 200 && resp.status !== 503) { const host = document.getElementById('scannersTable'); if (host) host.innerHTML = - `
Failed to load scanners (HTTP ${scannersResp.status}).
`; + `
Failed to load health (HTTP ${resp.status}).
`; + return; } - if (agentsResp.ok) { - const data = await agentsResp.json(); - for (const agent of (data.agents || [])) applyAgentRow(agent); - } + const data = await resp.json(); + renderScanners(data.scanners || {}); + for (const agent of ((data.edr_agents || {}).agents || [])) applyAgentRow(agent); } catch (err) { console.error('[dashboard] refresh failed:', err); } finally { @@ -175,9 +171,8 @@ document.addEventListener('DOMContentLoaded', () => { }); // Pause the auto-refresh while the tab is hidden — no point pulling -// /api/system/scanners + /api/edr/agents/status every minute when nobody's -// looking. Resume on visible AND fire one immediate refresh so the user -// sees fresh data the moment they come back. +// /health every minute when nobody's looking. Resume on visible AND fire +// one immediate refresh so the user sees fresh data the moment they come back. document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { stopTimer();