mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
430 lines
19 KiB
HTML
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 & 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>
|