mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 15:54:07 +00:00
Compare commits
No commits in common. "65a1a8e494597b1ccb87af612e9c9f0230d0e69b" and "91cba0bbda66214bce004c57093265e0263a00d1" have entirely different histories.
65a1a8e494
...
91cba0bbda
|
|
@ -3,33 +3,6 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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
|
## 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,
|
Large feature sprint on `secubox-toolbox` (built + merged + deployed live,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ Features:
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
@ -728,125 +727,8 @@ async def rollback_rule(rule_id: str):
|
||||||
# Startup
|
# 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")
|
@app.on_event("startup")
|
||||||
async def startup():
|
async def startup():
|
||||||
"""Initialize on startup."""
|
"""Initialize on startup."""
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
asyncio.create_task(_overview_refresh_loop())
|
|
||||||
logger.info("Threat Analyst started")
|
logger.info("Threat Analyst started")
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,3 @@
|
||||||
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
|
secubox-threat-analyst (1.4.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* fix(postinst): build-safe service enable (#595). A plain `systemctl
|
* fix(postinst): build-safe service enable (#595). A plain `systemctl
|
||||||
|
|
|
||||||
|
|
@ -262,31 +262,6 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Top-N leaderboards — the real insight surface -->
|
||||||
<div class="top-row">
|
<div class="top-row">
|
||||||
<div class="top-box">
|
<div class="top-box">
|
||||||
|
|
@ -580,29 +555,8 @@
|
||||||
setAutoStatus('', `collected ${c.crowdsec ?? 0} cs · ${c.waf ?? 0} waf — ${new Date().toLocaleTimeString()}`);
|
setAutoStatus('', `collected ${c.crowdsec ?? 0} cs · ${c.waf ?? 0} waf — ${new Date().toLocaleTimeString()}`);
|
||||||
await loadAll();
|
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() {
|
async function loadAll() {
|
||||||
await Promise.all([loadOverview(), loadStats(), loadAlerts(), loadRules(), loadEmancipated()]);
|
await Promise.all([loadStats(), loadAlerts(), loadRules(), loadEmancipated()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostic
|
// Diagnostic
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user