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:
|
def get_scanners_status(self) -> Dict:
|
||||||
"""Inventory of configured analyzers and whether their binaries
|
"""Inventory of configured analyzers and whether their binaries
|
||||||
exist on disk (drives the dashboard scanner panel)."""
|
exist on disk. Returns the `scanners` field of the unified /health
|
||||||
response = self._make_request("GET", "/api/system/scanners")
|
response: `{rows: [...], counts: {...}}`."""
|
||||||
return response.json()
|
return (self.check_health() or {}).get("scanners", {"rows": [], "counts": {}})
|
||||||
|
|
||||||
# ---- destructive ---------------------------------------------------
|
# ---- destructive ---------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -111,53 +111,6 @@ def api_edr_profiles():
|
|||||||
return jsonify({'profiles': deps.edr_registry.list_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'])
|
@api_bp.route('/api/edr/fibratus/<profile>/alerts/since', methods=['GET'])
|
||||||
@error_handler
|
@error_handler
|
||||||
def api_fibratus_alerts_passthrough(profile):
|
def api_fibratus_alerts_passthrough(profile):
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ from datetime import datetime
|
|||||||
from flask import Blueprint, current_app, jsonify
|
from flask import Blueprint, current_app, jsonify
|
||||||
|
|
||||||
from ..services.error_handling import error_handler
|
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
|
from ..utils import path_manager
|
||||||
|
|
||||||
management_bp = Blueprint('management', __name__)
|
management_bp = Blueprint('management', __name__)
|
||||||
@@ -100,6 +104,16 @@ def cleanup():
|
|||||||
@management_bp.route('/health', methods=['GET'])
|
@management_bp.route('/health', methods=['GET'])
|
||||||
@error_handler
|
@error_handler
|
||||||
def health_check():
|
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 = current_app
|
||||||
app.logger.debug("Starting health check.")
|
app.logger.debug("Starting health check.")
|
||||||
config = app.config
|
config = app.config
|
||||||
@@ -127,15 +141,11 @@ def health_check():
|
|||||||
|
|
||||||
check_holygrail_tool(holygrail_section, issues, app.logger)
|
check_holygrail_tool(holygrail_section, issues, app.logger)
|
||||||
|
|
||||||
static_tools = {
|
scanner_rows, scanner_counts = scanner_inventory(analysis_config)
|
||||||
tool: static_section.get(tool, {}).get('enabled', False)
|
|
||||||
for tool in static_section.keys()
|
deps = current_app.extensions['litterbox']
|
||||||
}
|
profiles = list(deps.edr_registry._PROFILES.values())
|
||||||
dynamic_tools = {
|
edr_snapshot = edr_health.get_status_snapshot(profiles)
|
||||||
tool: dynamic_section.get(tool, {}).get('enabled', False)
|
|
||||||
for tool in dynamic_section.keys()
|
|
||||||
}
|
|
||||||
holygrail_status = holygrail_section.get('enabled', False)
|
|
||||||
|
|
||||||
status = 'ok' if not issues else 'degraded'
|
status = 'ok' if not issues else 'degraded'
|
||||||
app.logger.debug(f"Health check completed. Status: {status}")
|
app.logger.debug(f"Health check completed. Status: {status}")
|
||||||
@@ -143,13 +153,15 @@ def health_check():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'status': status,
|
'status': status,
|
||||||
'timestamp': datetime.now().isoformat(),
|
'timestamp': datetime.now().isoformat(),
|
||||||
'upload_folder_accessible': os.path.isdir(upload_folder) if upload_folder else False,
|
|
||||||
'issues': issues,
|
'issues': issues,
|
||||||
'configuration': {
|
'sandbox': {
|
||||||
'static_analysis': static_tools,
|
'upload_folder_accessible': os.path.isdir(upload_folder) if upload_folder else False,
|
||||||
'dynamic_analysis': dynamic_tools,
|
|
||||||
'holygrail_analysis': holygrail_status,
|
|
||||||
},
|
},
|
||||||
|
'scanners': {
|
||||||
|
'rows': scanner_rows,
|
||||||
|
'counts': scanner_counts,
|
||||||
|
},
|
||||||
|
'edr_agents': edr_snapshot,
|
||||||
}), 200 if status == 'ok' else 503
|
}), 200 if status == 'ok' else 503
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ upload_bp = Blueprint('upload', __name__)
|
|||||||
@upload_bp.route('/')
|
@upload_bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""System health dashboard: agents + scanner availability.
|
"""System health dashboard: agents + scanner availability.
|
||||||
Live data is fetched async by the page's JS via /api/edr/agents/status
|
Live data is fetched async by the page's JS via /health (single
|
||||||
and /api/system/scanners."""
|
request returns scanner inventory + EDR agent reachability)."""
|
||||||
deps = current_app.extensions['litterbox']
|
deps = current_app.extensions['litterbox']
|
||||||
return render_template(
|
return render_template(
|
||||||
'dashboard.html',
|
'dashboard.html',
|
||||||
|
|||||||
@@ -1,8 +1,53 @@
|
|||||||
# app/services/tool_check.py
|
# app/services/tool_check.py
|
||||||
"""Tool/path validation helpers for the /health endpoint."""
|
"""Tool/path validation + inventory helpers for the /health endpoint."""
|
||||||
import os
|
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):
|
def check_analysis_tool(section, tool_name, issues, logger):
|
||||||
"""Append issues for a static or dynamic analysis tool."""
|
"""Append issues for a static or dynamic analysis tool."""
|
||||||
tool_config = section.get(tool_name, {})
|
tool_config = section.get(tool_name, {})
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
// app/static/js/dashboard/core.js
|
// app/static/js/dashboard/core.js
|
||||||
//
|
//
|
||||||
// Drives the index dashboard. Polls /api/system/scanners for analyzer
|
// Drives the index dashboard. Single /health fetch returns scanner
|
||||||
// availability and /api/edr/agents/status for live agent + Elastic state.
|
// inventory + live EDR agent reachability in one shot. Auto-refreshes
|
||||||
// Auto-refreshes every 60s; the manual Refresh button forces an immediate
|
// every 60s; the manual Refresh button forces an immediate poll.
|
||||||
// poll. Both fetches are kicked off in parallel.
|
|
||||||
|
|
||||||
const REFRESH_MS = 60000;
|
const REFRESH_MS = 60000;
|
||||||
let _refreshTimer = null;
|
let _refreshTimer = null;
|
||||||
@@ -27,7 +26,7 @@ function statusTagClass(status) {
|
|||||||
function renderScanners(payload) {
|
function renderScanners(payload) {
|
||||||
const host = document.getElementById('scannersTable');
|
const host = document.getElementById('scannersTable');
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
const scanners = (payload && payload.scanners) || [];
|
const scanners = (payload && payload.rows) || [];
|
||||||
const counts = (payload && payload.counts) || {};
|
const counts = (payload && payload.counts) || {};
|
||||||
setText('scannersCount',
|
setText('scannersCount',
|
||||||
`${counts.ok ?? 0} ok · ${counts.missing ?? 0} missing · ${counts.disabled ?? 0} disabled`);
|
`${counts.ok ?? 0} ok · ${counts.missing ?? 0} missing · ${counts.disabled ?? 0} disabled`);
|
||||||
@@ -119,23 +118,20 @@ async function refreshDashboard() {
|
|||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [scannersResp, agentsResp] = await Promise.all([
|
// Single /health fetch returns both scanner inventory and EDR
|
||||||
fetch('/api/system/scanners', { cache: 'no-store' }),
|
// reachability. /health responds 200 (ok) or 503 (degraded) — both
|
||||||
fetch('/api/edr/agents/status', { cache: 'no-store' }),
|
// 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) {
|
||||||
if (scannersResp.ok) {
|
|
||||||
renderScanners(await scannersResp.json());
|
|
||||||
} else {
|
|
||||||
const host = document.getElementById('scannersTable');
|
const host = document.getElementById('scannersTable');
|
||||||
if (host) host.innerHTML =
|
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 resp.json();
|
||||||
const data = await agentsResp.json();
|
renderScanners(data.scanners || {});
|
||||||
for (const agent of (data.agents || [])) applyAgentRow(agent);
|
for (const agent of ((data.edr_agents || {}).agents || [])) applyAgentRow(agent);
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[dashboard] refresh failed:', err);
|
console.error('[dashboard] refresh failed:', err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -175,9 +171,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Pause the auto-refresh while the tab is hidden — no point pulling
|
// Pause the auto-refresh while the tab is hidden — no point pulling
|
||||||
// /api/system/scanners + /api/edr/agents/status every minute when nobody's
|
// /health every minute when nobody's looking. Resume on visible AND fire
|
||||||
// looking. Resume on visible AND fire one immediate refresh so the user
|
// one immediate refresh so the user sees fresh data the moment they come back.
|
||||||
// sees fresh data the moment they come back.
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden') {
|
||||||
stopTimer();
|
stopTimer();
|
||||||
|
|||||||
Reference in New Issue
Block a user