mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 14:10:44 +00:00
Compare commits
4 Commits
91cba0bbda
...
65a1a8e494
| Author | SHA1 | Date | |
|---|---|---|---|
| 65a1a8e494 | |||
|
|
8fc5dba929 | ||
| 919a88c6be | |||
| 60eeb79185 |
|
|
@ -3,6 +3,33 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-06-15 — threat-analyst: global security overview (1.4.3, live on gk2)
|
||||
|
||||
`secubox-threat-analyst` 1.4.1 → 1.4.3, merged via **PR #598 (closes #597)**,
|
||||
built + deployed live on gk2.
|
||||
|
||||
- **#597** — threat-analyst page becomes a **global security overview**: all
|
||||
metrics dynamic, fed live from WAF + CrowdSec + firewall. New cached
|
||||
`/overview` endpoint (double-buffer, 60 s background refresh →
|
||||
`overview.json`) aggregating WAF (`/run/secubox/waf.sock /stats`: threats
|
||||
today, blocked 24 h, rules loaded), CrowdSec (detection: alerts), firewall
|
||||
(enforcement: IPs blocked in nft via crowdsec-firewall-bouncer). WebUI gains
|
||||
a "Vue globale sécurité" card row + source health line (`loadOverview()` in
|
||||
`loadAll()`).
|
||||
- **Privilege-safe sourcing**: daemon runs as unprivileged `secubox` user →
|
||||
`cscli`/`nft list` (both root-only) failed silently. Switched to CrowdSec's
|
||||
privilege-free **Prometheus :6060** (`cs_alerts` + `cs_active_decisions`).
|
||||
No privilege escalation, no coupling to broken `secubox-blacklist-sync`.
|
||||
- Also carried the **1.4.2 build-safe postinst** fix (#595/#596) which had
|
||||
not yet reached the board (was at 1.4.1; `deb-systemd-helper` enable).
|
||||
- Live verified: CrowdSec 3712 alerts / 29312 active decisions, firewall
|
||||
29312 blocked, WAF 140 rules; `/overview` 200 via socket **and** aggregator
|
||||
proxy (aggregator restarted to re-discover the new route).
|
||||
|
||||
**Found, not fixed (separate):** `secubox-blacklist-sync.service` is **failed**
|
||||
(#521, exit 2) → `secubox_blacklist` nft sets empty. Does not affect the
|
||||
overview (firewall count comes from the bouncer via Prometheus).
|
||||
|
||||
## 2026-06-14 — ToolBoX privacy/perf sprint : 2.6.23 → 2.6.36, all live on gk2
|
||||
|
||||
Large feature sprint on `secubox-toolbox` (built + merged + deployed live,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Features:
|
|||
import os
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
|
|
@ -727,8 +728,125 @@ async def rollback_rule(rule_id: str):
|
|||
# Startup
|
||||
# ============================================================================
|
||||
|
||||
# ============================================================================
|
||||
# #597 — Global security overview : aggregate live metrics from WAF +
|
||||
# CrowdSec + the nft firewall. Double-cache pattern (CLAUDE perf rule) :
|
||||
# a background task refreshes every 60 s into _OVERVIEW so /overview is
|
||||
# instant and never blocks on cscli/nft subprocesses. Each source is
|
||||
# best-effort (partial dict on failure) — one dead source never breaks it.
|
||||
# ============================================================================
|
||||
|
||||
_OVERVIEW: Dict[str, Any] = {}
|
||||
_OVERVIEW_FILE = DATA_DIR / "overview.json"
|
||||
_OVERVIEW_TTL = 60
|
||||
|
||||
|
||||
async def _waf_overview() -> Dict[str, Any]:
|
||||
"""WAF /stats over its unix socket."""
|
||||
try:
|
||||
transport = httpx.AsyncHTTPTransport(uds="/run/secubox/waf.sock")
|
||||
async with httpx.AsyncClient(transport=transport, timeout=4) as c:
|
||||
r = await c.get("http://waf/stats")
|
||||
if r.status_code == 200:
|
||||
s = r.json()
|
||||
return {
|
||||
"running": bool(s.get("running")),
|
||||
"threats_today": s.get("threats_today", 0),
|
||||
"threats_total": s.get("total_threats", 0),
|
||||
"blocked_24h": s.get("blocked_24h", 0),
|
||||
"rules_loaded": s.get("rules_loaded", 0),
|
||||
"by_category": s.get("by_category", {}),
|
||||
"by_severity": s.get("by_severity", {}),
|
||||
"top_countries": s.get("top_countries", [])[:5],
|
||||
"top_vhosts": s.get("top_vhosts", [])[:5],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug("waf overview failed: %s", e)
|
||||
return {"running": False}
|
||||
|
||||
|
||||
# CrowdSec exposes a privilege-free Prometheus endpoint on :6060. We parse it
|
||||
# instead of shelling out to `cscli`/`nft` (both need root — this daemon runs as
|
||||
# the unprivileged `secubox` user, CSPN least-privilege). This gives us both the
|
||||
# detection layer (cs_alerts) and the enforcement layer (cs_active_decisions,
|
||||
# which the crowdsec-firewall-bouncer materializes into nft) from one HTTP GET.
|
||||
_PROM_URL = "http://127.0.0.1:6060/metrics"
|
||||
|
||||
|
||||
def _prom_sum(text: str, prefix: str) -> int:
|
||||
"""Sum the values of every Prometheus sample line starting with prefix."""
|
||||
total = 0.0
|
||||
for line in text.splitlines():
|
||||
if not line.startswith(prefix) or line.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
total += float(line.rsplit(" ", 1)[1])
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
return int(total)
|
||||
|
||||
|
||||
async def _crowdsec_firewall_overview():
|
||||
"""One privilege-free fetch of CrowdSec Prometheus → (crowdsec, firewall).
|
||||
|
||||
crowdsec : detection layer — alerts + active decisions
|
||||
firewall : enforcement layer — IPs blocked in nft via crowdsec-firewall-bouncer
|
||||
"""
|
||||
cs: Dict[str, Any] = {"running": False, "active_decisions": 0, "alerts": 0}
|
||||
fw: Dict[str, Any] = {"running": False, "blocked": 0,
|
||||
"source": "crowdsec-firewall-bouncer (nft)"}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=4) as c:
|
||||
r = await c.get(_PROM_URL)
|
||||
if r.status_code == 200:
|
||||
active = _prom_sum(r.text, "cs_active_decisions")
|
||||
alerts = _prom_sum(r.text, "cs_alerts")
|
||||
cs = {"running": True, "active_decisions": active, "alerts": alerts}
|
||||
fw = {"running": True, "blocked": active,
|
||||
"source": "crowdsec-firewall-bouncer (nft)"}
|
||||
except Exception as e:
|
||||
logger.debug("crowdsec prometheus overview failed: %s", e)
|
||||
return cs, fw
|
||||
|
||||
|
||||
async def _build_overview() -> Dict[str, Any]:
|
||||
waf, (cs, fw) = await asyncio.gather(
|
||||
_waf_overview(),
|
||||
_crowdsec_firewall_overview(),
|
||||
)
|
||||
return {"waf": waf, "crowdsec": cs, "firewall": fw, "updated": int(time.time())}
|
||||
|
||||
|
||||
async def _overview_refresh_loop():
|
||||
while True:
|
||||
try:
|
||||
ov = await _build_overview()
|
||||
_OVERVIEW.clear(); _OVERVIEW.update(ov)
|
||||
try:
|
||||
_OVERVIEW_FILE.write_text(json.dumps(ov))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning("overview refresh failed: %s", e)
|
||||
await asyncio.sleep(_OVERVIEW_TTL)
|
||||
|
||||
|
||||
@app.get("/overview")
|
||||
async def get_overview():
|
||||
"""Global security overview (WAF + CrowdSec + firewall), 60 s cached."""
|
||||
if _OVERVIEW:
|
||||
return _OVERVIEW
|
||||
if _OVERVIEW_FILE.exists():
|
||||
try:
|
||||
return json.loads(_OVERVIEW_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
return await _build_overview()
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
"""Initialize on startup."""
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
asyncio.create_task(_overview_refresh_loop())
|
||||
logger.info("Threat Analyst started")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,16 @@
|
|||
secubox-threat-analyst (1.4.3-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* feat(overview): global security overview — all metrics now dynamic,
|
||||
fed live from WAF + CrowdSec + firewall (#597). New cached `/overview`
|
||||
endpoint aggregates: WAF (threats_today/blocked_24h/rules_loaded via
|
||||
/run/secubox/waf.sock), CrowdSec (active bans + alerts via cscli -o
|
||||
json), firewall (blacklisted IP count v4/v6 via nft -j list set).
|
||||
Double-buffer background refresh (60s, overview.json) + source
|
||||
health line. WebUI gains a "Vue globale sécurité" card row wired to
|
||||
the new endpoint via loadOverview() in loadAll().
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Mon, 15 Jun 2026 10:00:00 +0200
|
||||
|
||||
secubox-threat-analyst (1.4.2-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* fix(postinst): build-safe service enable (#595). A plain `systemctl
|
||||
|
|
|
|||
|
|
@ -262,6 +262,31 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- #597 Global security overview — live from WAF + CrowdSec + firewall -->
|
||||
<h3 style="margin:1rem 0 .4rem">🛡 Vue globale sécurité <span id="ov-sources" style="font-size:.7rem;color:var(--text-muted)"></span></h3>
|
||||
<div class="stats">
|
||||
<div class="stat warn">
|
||||
<div class="stat-label">WAF — menaces (24h)</div>
|
||||
<div class="stat-value" id="o-waf-threats">—</div>
|
||||
<div class="stat-hint" id="o-waf-blocked">bloquées: —</div>
|
||||
</div>
|
||||
<div class="stat crit">
|
||||
<div class="stat-label">CrowdSec — détections</div>
|
||||
<div class="stat-value" id="o-cs-bans">—</div>
|
||||
<div class="stat-hint" id="o-cs-alerts">alertes: —</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Firewall — IP bloquées (nft)</div>
|
||||
<div class="stat-value" id="o-fw-blacklist">—</div>
|
||||
<div class="stat-hint" id="o-fw-split">via crowdsec-bouncer</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">WAF — règles chargées</div>
|
||||
<div class="stat-value" id="o-waf-rules">—</div>
|
||||
<div class="stat-hint">moteur d'inspection</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top-N leaderboards — the real insight surface -->
|
||||
<div class="top-row">
|
||||
<div class="top-box">
|
||||
|
|
@ -555,8 +580,29 @@
|
|||
setAutoStatus('', `collected ${c.crowdsec ?? 0} cs · ${c.waf ?? 0} waf — ${new Date().toLocaleTimeString()}`);
|
||||
await loadAll();
|
||||
}
|
||||
// #597 — global overview from WAF + CrowdSec + firewall
|
||||
async function loadOverview() {
|
||||
const r = await get('/api/v1/threat-analyst/overview');
|
||||
diagSet('/overview', r);
|
||||
if (!r.ok) return;
|
||||
const o = r.data || {}, w = o.waf || {}, c = o.crowdsec || {}, f = o.firewall || {};
|
||||
const set = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; };
|
||||
set('o-waf-threats', w.threats_today ?? '—');
|
||||
set('o-waf-blocked', 'bloquées: ' + (w.blocked_24h ?? 0) + ' (24h)');
|
||||
set('o-cs-bans', c.alerts ?? '—');
|
||||
set('o-cs-alerts', 'décisions actives: ' + (c.active_decisions ?? 0));
|
||||
set('o-fw-blacklist', f.blocked ?? '—');
|
||||
set('o-fw-split', f.source || 'via crowdsec-bouncer');
|
||||
set('o-waf-rules', w.rules_loaded ?? '—');
|
||||
const up = [];
|
||||
up.push((w.running ? '🟢' : '🔴') + ' WAF');
|
||||
up.push((c.running ? '🟢' : '🔴') + ' CrowdSec');
|
||||
up.push('🟢 Firewall');
|
||||
const ago = o.updated ? Math.max(0, Math.round(Date.now()/1000 - o.updated)) + 's' : '';
|
||||
set('ov-sources', up.join(' · ') + (ago ? ' · maj ' + ago : ''));
|
||||
}
|
||||
async function loadAll() {
|
||||
await Promise.all([loadStats(), loadAlerts(), loadRules(), loadEmancipated()]);
|
||||
await Promise.all([loadOverview(), loadStats(), loadAlerts(), loadRules(), loadEmancipated()]);
|
||||
}
|
||||
|
||||
// Diagnostic
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user