secubox-deb/packages/secubox-portal/www/portal/index.html
CyberMind-FR 877fb9e19a
Some checks are pending
License Headers / check (push) Waiting to run
fix(portal): bind Active bans to /crowdsec/decisions count (2.2.2)
2026-06-29 17:25:16 +02:00

430 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SecuBox · Portal</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>">
<link rel="stylesheet" href="/shared/design-tokens.css">
<link rel="stylesheet" href="/shared/crt-light.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
a { color: var(--cyber-cyan, #00d4ff); text-decoration: none; }
a:hover { color: var(--gold-hermetic, #c9a84c); }
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
/* Hero */
.hero {
margin-bottom: 2rem;
padding: 1.5rem 2rem;
background: var(--panel, #13131c);
border: 1px solid var(--line, #2a2a3a);
border-radius: 4px;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 2rem;
}
.hero-score {
display: flex; flex-direction: column; align-items: center;
min-width: 140px;
}
.hero-score .num {
font-size: 3.5rem; font-weight: bold; line-height: 1;
color: var(--matrix-green, #00ff41);
}
.hero-score .label { font-size: 0.8rem; color: var(--text-muted, #6b6b7a); margin-top: 0.25rem; letter-spacing: 1px; }
.hero-score.warn .num { color: var(--gold-hermetic, #c9a84c); }
.hero-score.crit .num { color: var(--cinnabar, #e63946); }
.hero-info { font-size: 0.95rem; line-height: 1.6; }
.hero-info .kv { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.hero-info .k { color: var(--text-muted, #6b6b7a); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; }
.hero-info .v { color: var(--text, #e8e6d9); font-weight: bold; }
.hero-refresh { font-size: 0.75rem; color: var(--text-muted, #6b6b7a); text-align: right; }
/* Section grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.card {
background: var(--panel, #13131c);
border: 1px solid var(--line, #2a2a3a);
border-radius: 4px;
padding: 1.25rem;
position: relative;
}
.card h3 {
font-size: 0.85rem; text-transform: uppercase; letter-spacing: 2px;
color: var(--gold-hermetic, #c9a84c); margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px dashed var(--line, #2a2a3a);
}
.card .row { display: flex; justify-content: space-between; padding: 0.25rem 0; font-size: 0.9rem; }
.card .row .k { color: var(--text-muted, #6b6b7a); }
.card .row .v { color: var(--text, #e8e6d9); font-weight: bold; }
.card .row .v.ok { color: var(--matrix-green, #00ff41); }
.card .row .v.warn { color: var(--gold-hermetic, #c9a84c); }
.card .row .v.crit { color: var(--cinnabar, #e63946); }
.empty { color: var(--text-muted, #6b6b7a); font-style: italic; font-size: 0.85rem; padding: 0.5rem 0; }
/* Bar */
.bar {
height: 4px; background: var(--cosmos-black, #0a0a0f); border-radius: 2px;
overflow: hidden; margin: 0.25rem 0;
}
.bar > span { display: block; height: 100%; background: var(--matrix-green, #00ff41); }
.bar.warn > span { background: var(--gold-hermetic, #c9a84c); }
.bar.crit > span { background: var(--cinnabar, #e63946); }
/* Modules LED grid */
.modules { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.5rem; }
.module {
display: flex; flex-direction: column; align-items: center;
gap: 0.25rem; padding: 0.5rem; border-radius: 4px;
background: var(--cosmos-black, #0a0a0f); border: 1px solid var(--line, #2a2a3a);
font-size: 0.7rem;
}
.module .led {
width: 10px; height: 10px; border-radius: 50%;
background: var(--text-muted, #6b6b7a);
}
.module.ok .led { background: var(--matrix-green, #00ff41); box-shadow: 0 0 6px var(--matrix-green, #00ff41); }
.module.warn .led { background: var(--gold-hermetic, #c9a84c); box-shadow: 0 0 6px var(--gold-hermetic, #c9a84c); }
.module.err .led { background: var(--cinnabar, #e63946); box-shadow: 0 0 6px var(--cinnabar, #e63946); }
.module .name { font-size: 0.65rem; text-transform: uppercase; color: var(--text-muted, #6b6b7a); }
/* Tables — vhosts / ASNs */
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th, td { text-align: left; padding: 0.25rem 0.5rem; }
th { font-size: 0.7rem; color: var(--text-muted, #6b6b7a); text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px dashed var(--line, #2a2a3a); }
td.num { text-align: right; color: var(--cyber-cyan, #00d4ff); font-variant-numeric: tabular-nums; }
tr:hover td { background: rgba(201,168,76,0.05); }
/* Big number tiles */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; }
.tile {
background: var(--cosmos-black, #0a0a0f); border: 1px solid var(--line, #2a2a3a);
border-radius: 4px; padding: 0.75rem; text-align: center;
}
.tile .v { font-size: 1.75rem; font-weight: bold; color: var(--matrix-green, #00ff41); }
.tile .v.warn { color: var(--gold-hermetic, #c9a84c); }
.tile .v.crit { color: var(--cinnabar, #e63946); }
.tile .l { font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted, #6b6b7a); letter-spacing: 1px; }
.footer {
margin-top: 3rem; padding-top: 1.5rem;
border-top: 1px dashed var(--line, #2a2a3a);
text-align: center; font-size: 0.8rem; color: var(--text-muted, #6b6b7a);
}
.footer a { color: var(--cyber-cyan, #00d4ff); }
@media (max-width: 768px) {
.hero { grid-template-columns: 1fr; }
.modules { grid-template-columns: repeat(3, 1fr); }
}
</style>
</head>
<body class="crt-light">
<nav class="sidebar" id="sidebar"></nav>
<script src="/shared/sidebar.js"></script>
<main class="main">
<div class="container">
<!-- Hero -->
<section class="hero">
<div class="hero-score" id="hero-score">
<div class="num" id="hero-score-num"></div>
<div class="label">HEALTH</div>
</div>
<div class="hero-info">
<div class="kv">
<div><div class="k">Hostname</div><div class="v" id="hero-hostname"></div></div>
<div><div class="k">Uptime</div><div class="v" id="hero-uptime"></div></div>
<div><div class="k">SSL</div><div class="v" id="hero-ssl"></div></div>
<div><div class="k">VHosts</div><div class="v" id="hero-vhosts"></div></div>
<div><div class="k">Certificates</div><div class="v" id="hero-certs"></div></div>
<div><div class="k">Bans active</div><div class="v" id="hero-bans"></div></div>
</div>
</div>
<div class="hero-refresh">
<div>last refresh</div>
<div id="hero-refresh"></div>
</div>
</section>
<!-- Module status LEDs -->
<section class="card" style="margin-bottom: 1.5rem;">
<h3>SERVICES</h3>
<div class="modules" id="modules-grid">
<div class="empty" style="grid-column: 1/-1;">Loading…</div>
</div>
</section>
<div class="grid">
<!-- System -->
<div class="card">
<h3>System</h3>
<div id="sys-rows"><div class="empty">Loading…</div></div>
</div>
<!-- Certs -->
<div class="card">
<h3>Certificates</h3>
<div id="cert-rows"><div class="empty">Loading…</div></div>
</div>
<!-- Cookie audit / RGPD -->
<div class="card">
<h3>Cookie Audit · RGPD</h3>
<div id="cookie-rows"><div class="empty">Loading…</div></div>
</div>
<!-- Bans (crowdsec decisions count + WAF %) -->
<div class="card">
<h3>Attacks &amp; Bans</h3>
<div class="tiles" id="bans-tiles">
<div class="tile"><div class="v" id="t-bans"></div><div class="l">Active bans</div></div>
<div class="tile"><div class="v" id="t-waf"></div><div class="l">WAF blocked %</div></div>
<div class="tile"><div class="v" id="t-alerts"></div><div class="l">Today alerts</div></div>
</div>
</div>
</div>
<div class="grid">
<!-- Vhosts (live-hosts top) -->
<div class="card">
<h3>Top Vhosts (last 60 min)</h3>
<div id="vhosts-table"><div class="empty">Loading…</div></div>
</div>
<!-- Attackers (visitor-origin top ASNs) -->
<div class="card">
<h3>Top Visitor ASN</h3>
<div id="asn-table"><div class="empty">Loading…</div></div>
</div>
</div>
<footer class="footer">
<p>SecuBox © 2024-2026 · <a href="https://cybermind.fr">CyberMind</a> · <a href="https://github.com/CyberMind-FR/secubox-deb">source</a></p>
<p style="margin-top: 0.5rem;">Public Portal · <span id="footer-hostname"></span> · auto-refresh 30 s</p>
</footer>
</div>
</main>
<script src="/shared/crt-engine.js"></script>
<script>
(function () {
'use strict';
const REFRESH_MS = 30000;
const ENDPOINTS = {
health: '/api/v1/metrics/health/summary',
certs: '/api/v1/metrics/cert-status',
vhosts: '/api/v1/metrics/live-hosts',
asns: '/api/v1/metrics/visitor-origin',
cookies: '/api/v1/cookie-audit/summary',
decisions: '/api/v1/crowdsec/decisions',
};
function $(id) { return document.getElementById(id); }
function txt(id, v) { const e = $(id); if (e) e.textContent = (v === undefined || v === null) ? '—' : v; }
function fmtUptime(s) {
if (!s) return '—';
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
return `${d}d ${String(h).padStart(2,'0')}h${String(m).padStart(2,'0')}`;
}
function fmtPct(v) {
if (v === null || v === undefined) return '—';
return Number(v).toFixed(0) + '%';
}
function fmtTime(ts) {
if (!ts) return '—';
try { return new Date(ts).toLocaleTimeString('fr-FR'); } catch (e) { return '—'; }
}
async function fetchSafe(url) {
try {
const r = await fetch(url, { credentials: 'omit' });
if (!r.ok) return null;
return await r.json();
} catch (e) { return null; }
}
function renderHero(h, c) {
if (!h) return;
const score = h.score || 0;
txt('hero-score-num', score + '%');
const sc = $('hero-score');
sc.classList.remove('warn', 'crit');
if (score < 70) sc.classList.add('crit');
else if (score < 85) sc.classList.add('warn');
txt('hero-hostname', location.hostname);
txt('nav-hostname', location.hostname);
txt('footer-hostname', location.hostname);
txt('hero-uptime', fmtUptime(h.system && h.system.uptime));
if (h.ssl) {
const d = h.ssl.days_remaining;
txt('hero-ssl', d == null ? '—' : `${d}j (${h.ssl.status || '?'})`);
}
txt('hero-vhosts', h.counts && h.counts.vhosts);
txt('hero-certs', (c && c.summary && c.summary.total) || (h.counts && h.counts.certificates) || '—');
txt('hero-bans', h.crowdsec && h.crowdsec.active_decisions);
txt('hero-refresh', fmtTime(h.timestamp || new Date().toISOString()));
}
function renderModules(h) {
if (!h || !h.modules) return;
const order = ['waf', 'crowdsec', 'haproxy', 'nginx', 'system'];
const icons = { waf: '🛡️', crowdsec: '👮', haproxy: '🌐', nginx: '📡', system: '💻' };
const el = $('modules-grid');
el.innerHTML = order.map(m => {
const mod = h.modules[m] || {};
const status = mod.status || 'off';
const cls = status === 'ok' ? 'ok' : status === 'warn' ? 'warn' : status === 'error' ? 'err' : '';
return `<div class="module ${cls}">
<span class="led"></span>
<span style="font-size:1.4rem;line-height:1">${icons[m]||'📦'}</span>
<span class="name">${m}</span>
</div>`;
}).join('');
}
function renderSystem(h) {
if (!h || !h.system) return;
const s = h.system;
const row = (k, v, pct) => {
const cls = pct == null ? '' : (pct >= 85 ? 'crit' : pct >= 60 ? 'warn' : 'ok');
return `<div class="row"><span class="k">${k}</span><span class="v ${cls}">${v}</span></div>` +
(pct != null ? `<div class="bar ${cls}"><span style="width:${Math.min(100, pct)}%"></span></div>` : '');
};
$('sys-rows').innerHTML =
row('CPU', fmtPct(s.cpu), s.cpu) +
row('Memory', fmtPct(s.memory || s.mem_pct), (s.memory || s.mem_pct)) +
row('Disk', fmtPct(s.disk), s.disk) +
row('Load', s.load || '—', null) +
row('LXC', (h.services && h.services.lxc_running) || 0, null);
}
function renderCerts(c) {
if (!c || !c.summary) {
$('cert-rows').innerHTML = '<div class="empty">cert-status disabled</div>';
return;
}
const s = c.summary;
const next = c.next_renewal
? `<div class="row"><span class="k">next renewal</span><span class="v">${c.next_renewal.host} · ${c.next_renewal.days}d</span></div>`
: '';
$('cert-rows').innerHTML = `
<div class="row"><span class="k">Total</span><span class="v">${s.total || 0}</span></div>
<div class="row"><span class="k">Valid</span><span class="v ok">${s.valid || 0}</span></div>
<div class="row"><span class="k">Expiring soon</span><span class="v ${s.expiring_soon ? 'warn' : ''}">${s.expiring_soon || 0}</span></div>
<div class="row"><span class="k">Critical</span><span class="v ${s.expiring_critical ? 'crit' : ''}">${s.expiring_critical || 0}</span></div>
<div class="row"><span class="k">Expired</span><span class="v ${s.expired ? 'crit' : ''}">${s.expired || 0}</span></div>
${next}
`;
}
function renderCookies(d) {
if (!d || !d.enabled || !d.summary) {
$('cookie-rows').innerHTML = '<div class="empty">cookie audit disabled</div>';
return;
}
const s = d.summary;
const byCat = s.by_category || {};
const violCls = s.violation_count > 0 ? 'crit' : 'ok';
$('cookie-rows').innerHTML = `
<div class="row"><span class="k">Vhosts surveillés</span><span class="v">${s.host_count || 0}</span></div>
<div class="row"><span class="k">RGPD violations</span><span class="v ${violCls}">${s.violation_count || 0}</span></div>
<div class="row"><span class="k">Strictly necessary</span><span class="v">${byCat.strictly_necessary || 0}</span></div>
<div class="row"><span class="k">Functional</span><span class="v">${byCat.functional || 0}</span></div>
<div class="row"><span class="k">Analytics</span><span class="v ${byCat.analytics ? 'warn' : ''}">${byCat.analytics || 0}</span></div>
<div class="row"><span class="k">Marketing</span><span class="v ${byCat.marketing ? 'warn' : ''}">${byCat.marketing || 0}</span></div>
<div class="row"><span class="k">Unclassified</span><span class="v ${byCat.unclassified ? 'warn' : ''}">${byCat.unclassified || 0}</span></div>
`;
}
function renderBans(h, decisions) {
// Prefer the live /crowdsec/decisions list (includes CAPI community
// blocklist), falling back to health/summary's local-only count.
const decList = decisions && Array.isArray(decisions.decisions) ? decisions.decisions
: (Array.isArray(decisions) ? decisions : null);
const ban = decList ? decList.filter(d => (d.type || 'ban') === 'ban').length
: (h && h.crowdsec ? h.crowdsec.active_decisions : null);
const alerts = h && h.crowdsec && h.crowdsec.alerts_today;
const wafPct = h && h.waf && h.waf.blocked_pct;
const wafCls = wafPct == null ? '' : (wafPct >= 25 ? 'crit' : wafPct >= 10 ? 'warn' : 'ok');
const banCls = ban == null ? '' : (ban >= 50 ? 'warn' : 'ok');
$('t-bans').textContent = ban != null ? ban : '—';
$('t-bans').className = 'v ' + banCls;
$('t-waf').textContent = wafPct != null ? wafPct + '%' : '—';
$('t-waf').className = 'v ' + wafCls;
$('t-alerts').textContent = alerts != null ? alerts : '—';
// Keep the hero "Bans active" consistent with the real blocked-IP count.
if (ban != null && $('hero-bans')) $('hero-bans').textContent = ban;
}
function renderVhosts(d) {
if (!d || !d.enabled || !d.entries || !d.entries.length) {
$('vhosts-table').innerHTML = '<div class="empty">live-hosts disabled or no data</div>';
return;
}
$('vhosts-table').innerHTML = `<table>
<thead><tr><th>Host</th><th style="text-align:right">Requests</th></tr></thead>
<tbody>${d.entries.map(e => `<tr><td>${escapeHtml(e.host)}</td><td class="num">${e.count}</td></tr>`).join('')}</tbody>
</table>`;
}
function renderAsns(d) {
if (!d || !d.enabled || !d.entries || !d.entries.length) {
$('asn-table').innerHTML = '<div class="empty">visitor-origin disabled (missing GeoLite2-ASN.mmdb)</div>';
return;
}
$('asn-table').innerHTML = `<table>
<thead><tr><th>ASN</th><th>Org</th><th style="text-align:right">Hits</th></tr></thead>
<tbody>${d.entries.map(e => `<tr><td>AS${e.asn}</td><td>${escapeHtml(e.org)}</td><td class="num">${e.count}</td></tr>`).join('')}</tbody>
</table>`;
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s == null ? '' : String(s);
return div.innerHTML;
}
async function refresh() {
const [h, c, v, a, k, dec] = await Promise.all([
fetchSafe(ENDPOINTS.health),
fetchSafe(ENDPOINTS.certs),
fetchSafe(ENDPOINTS.vhosts),
fetchSafe(ENDPOINTS.asns),
fetchSafe(ENDPOINTS.cookies),
fetchSafe(ENDPOINTS.decisions),
]);
renderHero(h, c);
renderModules(h);
renderSystem(h);
renderCerts(c);
renderCookies(k);
renderBans(h, dec);
renderVhosts(v);
renderAsns(a);
}
refresh();
setInterval(refresh, REFRESH_MS);
})();
</script>
</body>
</html>