Unify /health: scanner inventory + EDR agents in one route
This commit is contained in:
@@ -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 ---------------------------------------------------
|
||||
|
||||
|
||||
@@ -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/<profile>/alerts/since', methods=['GET'])
|
||||
@error_handler
|
||||
def api_fibratus_alerts_passthrough(profile):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
@@ -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 =
|
||||
`<div class="lb-muted">Failed to load scanners (HTTP ${scannersResp.status}).</div>`;
|
||||
`<div class="lb-muted">Failed to load health (HTTP ${resp.status}).</div>`;
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user