mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 07:08:34 +00:00
Compare commits
4 Commits
434d3aba6a
...
f5da2f6aa8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5da2f6aa8 | ||
| d4c268e6e4 | |||
|
|
72514ca678 | ||
| a54ad6ab04 |
|
|
@ -71,6 +71,13 @@ ul{list-style:none;padding-left:.2rem}li{padding:.12rem 0;font-size:.82rem}li::b
|
||||||
.actions a{padding:.55rem 1rem;border:1px solid var(--phos);color:var(--phos-hot);text-decoration:none;border-radius:8px;font-size:.85rem}
|
.actions a{padding:.55rem 1rem;border:1px solid var(--phos);color:var(--phos-hot);text-decoration:none;border-radius:8px;font-size:.85rem}
|
||||||
.footer{text-align:center;font-size:.66rem;color:var(--dim);margin-top:1.4rem;border-top:1px solid var(--line);padding-top:.7rem}
|
.footer{text-align:center;font-size:.66rem;color:var(--dim);margin-top:1.4rem;border-top:1px solid var(--line);padding-top:.7rem}
|
||||||
.url{font-family:ui-monospace,monospace;font-size:.72rem;background:#0d0f15;padding:.12rem .35rem;border-radius:4px;margin:.1rem 0;display:block;word-break:break-all}
|
.url{font-family:ui-monospace,monospace;font-size:.72rem;background:#0d0f15;padding:.12rem .35rem;border-radius:4px;margin:.1rem 0;display:block;word-break:break-all}
|
||||||
|
/* ── tabs (#699) ── */
|
||||||
|
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--line);margin-bottom:1rem;flex-wrap:wrap}
|
||||||
|
.tabs button{background:transparent;border:0;color:var(--dim);padding:.55rem .9rem;font-family:inherit;font-size:.85rem;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}
|
||||||
|
.tabs button.active{color:var(--phos-hot);border-bottom-color:var(--phos);background:rgba(0,221,68,.06)}
|
||||||
|
.tabs button:hover{color:var(--text)}
|
||||||
|
.tab-pane{display:none}
|
||||||
|
.tab-pane.active{display:block}
|
||||||
</style></head>
|
</style></head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -89,9 +96,38 @@ ul{list-style:none;padding-left:.2rem}li{padding:.12rem 0;font-size:.82rem}li::b
|
||||||
{% set n_opgrade = gst.opgrade_sites|default(0) %}
|
{% set n_opgrade = gst.opgrade_sites|default(0) %}
|
||||||
{% set _avatar = avatar_analysis or {} %}
|
{% set _avatar = avatar_analysis or {} %}
|
||||||
|
|
||||||
|
{# #699 — reusable donut (conic-gradient + legend) for the DPI-Exfil/Overall tabs #}
|
||||||
|
{% macro donut(title, hole, items) %}
|
||||||
|
{% set pal = ['#00dd44','#9e76ff','#ff8866','#66bbff','#ffb347','#ff4466'] %}
|
||||||
|
<div>
|
||||||
|
<div style="font-size:.82rem;color:var(--dim);margin-bottom:.4rem">{{ title }}</div>
|
||||||
|
{% if items %}
|
||||||
|
<div class="donut-wrap">
|
||||||
|
<div class="donut" style="background:conic-gradient({% for t in items %}{{ pal[loop.index0 % pal|length] }} {{ t.start }}% {{ t.end }}%{% if not loop.last %},{% endif %}{% endfor %})">
|
||||||
|
<div class="donut-hole">{{ hole }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="legend">
|
||||||
|
{% for t in items %}
|
||||||
|
<span class="row"><span class="dot" style="background:{{ pal[loop.index0 % pal|length] }}"></span>{{ t.emoji }} {{ t.label[:14] }} <b style="color:var(--text)">{{ t.pct }}%</b></span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}<div class="empty">Pas de données</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
<h1>👁️ VILLAGE3B <span style="font-size:.8rem;color:var(--dim);font-weight:400">· mon rapport</span></h1>
|
<h1>👁️ VILLAGE3B <span style="font-size:.8rem;color:var(--dim);font-weight:400">· mon rapport</span></h1>
|
||||||
<p class="sub">Diagnostic live de ce que ton appareil envoie sur le réseau · anonyme · se rafraîchit tout seul</p>
|
<p class="sub">Diagnostic live de ce que ton appareil envoie sur le réseau · anonyme · se rafraîchit tout seul</p>
|
||||||
|
|
||||||
|
{# ── TABS (#699) : Pistage / DPI-Exfil / Overall ── #}
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="active" data-tab="pistage">🍪 Pistage</button>
|
||||||
|
<button data-tab="dpi">🛰️ DPI-Exfil</button>
|
||||||
|
<button data-tab="overall">🌍 Overall</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane active" id="pane-pistage">
|
||||||
|
|
||||||
{% if request_args and (request_args.get('welcome') or request_args.get('switched')) %}
|
{% if request_args and (request_args.get('welcome') or request_args.get('switched')) %}
|
||||||
<div class="card" style="border-color:var(--phos)">
|
<div class="card" style="border-color:var(--phos)">
|
||||||
<b style="color:var(--phos-hot)">{% if request_args.get('switched') %}🔄 Niveau changé{% else %}🎉 Bienvenue !{% endif %}</b> —
|
<b style="color:var(--phos-hot)">{% if request_args.get('switched') %}🔄 Niveau changé{% else %}🎉 Bienvenue !{% endif %}</b> —
|
||||||
|
|
@ -302,6 +338,57 @@ ul{list-style:none;padding-left:.2rem}li{padding:.12rem 0;font-size:.82rem}li::b
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
</div>{# /pane-pistage #}
|
||||||
|
|
||||||
|
{# ── DPI-EXFIL TAB : this device's egress (secubox-dpi) ── #}
|
||||||
|
{% set dme = (dpi_exfil or {}).me or {} %}
|
||||||
|
<div class="tab-pane" id="pane-dpi">
|
||||||
|
<div class="kpis">
|
||||||
|
<div class="kpi"><div class="e">🌐</div><div class="n">{{ dme.flows|default(0) }}</div><div class="l">flux</div></div>
|
||||||
|
<div class="kpi"><div class="e">⬆️</div><div class="n">{{ (dme.up|default(0)/1048576)|round(1) }}</div><div class="l">Mo envoyés</div></div>
|
||||||
|
<div class="kpi"><div class="e">⬇️</div><div class="n">{{ (dme.down|default(0)/1048576)|round(1) }}</div><div class="l">Mo reçus</div></div>
|
||||||
|
<div class="kpi"><div class="e">🛰️</div><div class="n" style="color:{{ 'var(--red)' if dme.alert_count else 'var(--phos-hot)' }}">{{ dme.alert_count|default(0) }}</div><div class="l">alertes exfil</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>🛰️ Ce que cet appareil envoie dehors (DPI R3)</h2>
|
||||||
|
{% if dme.present %}
|
||||||
|
<div class="graphs">
|
||||||
|
{{ donut('🏷️ Catégories de service', 'par flux', dme.categories) }}
|
||||||
|
{{ donut('📡 Protocoles', 'octets', dme.protocols) }}
|
||||||
|
{{ donut('🛰️ Alertes exfiltration', 'alertes', dme.alerts) }}
|
||||||
|
{{ donut('🎯 Top destinations (envoi)', '↑ octets', dme.destinations) }}
|
||||||
|
</div>
|
||||||
|
<p class="help">Sorties observées sur le tunnel R3 (wg-toolbox), classées par le moteur DPI. Une grosse part « ☁️ cloud / 📦 fichier / 🤖 IA / 💬 messagerie » avec beaucoup d'envoi = fuite de données potentielle.</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty">Aucune donnée DPI pour cet appareil — il faut surfer via le tunnel R3 (🧅) pour que le moteur observe ses sorties, et attendre une fenêtre de capture (~60 s).</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>{# /pane-dpi #}
|
||||||
|
|
||||||
|
{# ── OVERALL TAB : board-wide DPI (all devices) ── #}
|
||||||
|
{% set dall = (dpi_exfil or {}).all or {} %}
|
||||||
|
<div class="tab-pane" id="pane-overall">
|
||||||
|
<div class="kpis">
|
||||||
|
<div class="kpi"><div class="e">📟</div><div class="n">{{ dall.devices|default(0) }}</div><div class="l">appareils</div></div>
|
||||||
|
<div class="kpi"><div class="e">🌐</div><div class="n">{{ dall.flows|default(0) }}</div><div class="l">flux</div></div>
|
||||||
|
<div class="kpi"><div class="e">🛰️</div><div class="n" style="color:{{ 'var(--red)' if dall.alert_count else 'var(--phos-hot)' }}">{{ dall.alert_count|default(0) }}</div><div class="l">alertes</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>🌍 Vue d'ensemble du réseau (tous appareils R3)</h2>
|
||||||
|
{% if dall.categories or dall.protocols or dall.destinations %}
|
||||||
|
<div class="graphs">
|
||||||
|
{{ donut('🏷️ Catégories de service', 'par flux', dall.categories) }}
|
||||||
|
{{ donut('📡 Protocoles', 'octets', dall.protocols) }}
|
||||||
|
{{ donut('🛰️ Alertes exfiltration', 'alertes', dall.alerts) }}
|
||||||
|
{{ donut('🎯 Top destinations', 'octets', dall.destinations) }}
|
||||||
|
</div>
|
||||||
|
<p class="help">Agrégat de tous les appareils derrière le tunnel R3 — utile pour repérer un usage anormal à l'échelle du réseau.</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty">Pas encore de données DPI à l'échelle réseau (tunnel inactif ou première capture en cours).</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>{# /pane-overall #}
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="/report/me?mh={{ mac_hash }}">⬇ Télécharger le PDF</a>
|
<a href="/report/me?mh={{ mac_hash }}">⬇ Télécharger le PDF</a>
|
||||||
<a href="/social/me?mh={{ mac_hash }}">🕸️ Ma carto</a>
|
<a href="/social/me?mh={{ mac_hash }}">🕸️ Ma carto</a>
|
||||||
|
|
@ -322,4 +409,18 @@ ul{list-style:none;padding-left:.2rem}li{padding:.12rem 0;font-size:.82rem}li::b
|
||||||
<a href="https://github.com/CyberMind-FR/secubox-deb" style="color:var(--dim)">github.com/CyberMind-FR/secubox-deb</a> · <a href="https://cybermind.fr" style="color:var(--dim)">cybermind.fr</a>
|
<a href="https://github.com/CyberMind-FR/secubox-deb" style="color:var(--dim)">github.com/CyberMind-FR/secubox-deb</a> · <a href="https://cybermind.fr" style="color:var(--dim)">cybermind.fr</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// #699 tab switcher — persists the active tab across the 20s meta-refresh via #hash
|
||||||
|
(function(){
|
||||||
|
function show(id){
|
||||||
|
document.querySelectorAll('.tab-pane').forEach(function(p){p.classList.toggle('active',p.id==='pane-'+id)});
|
||||||
|
document.querySelectorAll('.tabs button').forEach(function(b){b.classList.toggle('active',b.dataset.tab===id)});
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.tabs button').forEach(function(b){
|
||||||
|
b.addEventListener('click',function(){ location.hash=b.dataset.tab; show(b.dataset.tab); });
|
||||||
|
});
|
||||||
|
var h=(location.hash||'').replace('#','');
|
||||||
|
if(h && document.getElementById('pane-'+h)) show(h);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|
|
||||||
|
|
@ -2386,6 +2386,99 @@ def _build_report_charts(graph: dict) -> dict:
|
||||||
return {"trackers": trackers, "countries": countries, "sites": sites}
|
return {"trackers": trackers, "countries": countries, "sites": sites}
|
||||||
|
|
||||||
|
|
||||||
|
# #699 — DPI exfil donuts for the kbin report (Pistage / DPI-Exfil / Overall
|
||||||
|
# tabs). Read straight from the secubox-dpi collector state (same wg-hash
|
||||||
|
# identity as the report's mac_hash). Fail-empty so the report renders before the
|
||||||
|
# first capture window.
|
||||||
|
_DPI_STATE_PATH = Path("/var/lib/secubox/dpi/state.json")
|
||||||
|
_DPI_CAT_EMOJI = {
|
||||||
|
"cloud": "☁️", "filehost": "📦", "messaging": "💬", "ai": "🤖",
|
||||||
|
"media": "🎬", "game": "🎮", "social": "👥", "adult": "🔞",
|
||||||
|
}
|
||||||
|
_DPI_ALERT_EMOJI = {
|
||||||
|
"exfil_volume": "⬆️", "new_cloud": "☁️", "beaconing": "📡",
|
||||||
|
"unclassified_external": "❔",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dpi_donut(items: list, n: int = 6) -> list:
|
||||||
|
"""top-N + pct + cumulative start/end for a CSS conic-gradient donut."""
|
||||||
|
items = [it for it in items if it.get("count")]
|
||||||
|
items.sort(key=lambda x: x["count"], reverse=True)
|
||||||
|
items = items[:n]
|
||||||
|
total = sum(x["count"] for x in items) or 1
|
||||||
|
cum = 0
|
||||||
|
for it in items:
|
||||||
|
it["pct"] = round(100 * it["count"] / total)
|
||||||
|
it["start"] = cum
|
||||||
|
cum += it["pct"]
|
||||||
|
it["end"] = cum
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def _dpi_stats(mac_hash: str | None) -> dict:
|
||||||
|
"""Build DPI donut data for THIS device (me) and board-wide (overall) from
|
||||||
|
the secubox-dpi collector state. Returns {me, all}, each with categories /
|
||||||
|
protocols / alerts / destinations donuts (+ summary counters)."""
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
st = json.loads(_DPI_STATE_PATH.read_text()) if _DPI_STATE_PATH.exists() else {}
|
||||||
|
except Exception:
|
||||||
|
st = {}
|
||||||
|
devices = st.get("devices") or []
|
||||||
|
|
||||||
|
def cats(bycat: dict) -> list:
|
||||||
|
return _dpi_donut([{"label": k, "emoji": _DPI_CAT_EMOJI.get(k, "🌐"),
|
||||||
|
"count": v} for k, v in (bycat or {}).items()])
|
||||||
|
|
||||||
|
def alerts(alist: list) -> list:
|
||||||
|
by: dict = {}
|
||||||
|
for a in alist or []:
|
||||||
|
k = a.get("kind") or "?"
|
||||||
|
by[k] = by.get(k, 0) + 1
|
||||||
|
return _dpi_donut([{"label": k.replace("_", " "),
|
||||||
|
"emoji": _DPI_ALERT_EMOJI.get(k, "⚠️"), "count": c}
|
||||||
|
for k, c in by.items()])
|
||||||
|
|
||||||
|
# ── this device ──
|
||||||
|
me = next((d for d in devices if d.get("device") == mac_hash), None) or {}
|
||||||
|
me_protos: dict = {}
|
||||||
|
me_dests: list = []
|
||||||
|
for s in (me.get("services") or []):
|
||||||
|
p = s.get("proto") or "unknown"
|
||||||
|
me_protos[p] = me_protos.get(p, 0) + int(s.get("up_bytes", 0) or 0) + int(s.get("down_bytes", 0) or 0)
|
||||||
|
me_dests.append({"label": s.get("service") or s.get("dst") or "?",
|
||||||
|
"emoji": _DPI_CAT_EMOJI.get(s.get("category"), "🌐"),
|
||||||
|
"count": int(s.get("up_bytes", 0) or 0)})
|
||||||
|
me_stats = {
|
||||||
|
"present": bool(me),
|
||||||
|
"flows": me.get("flows", 0), "up": me.get("up_bytes", 0), "down": me.get("down_bytes", 0),
|
||||||
|
"alert_count": len(me.get("alerts") or []),
|
||||||
|
"categories": cats(me.get("by_category")),
|
||||||
|
"protocols": _dpi_donut([{"label": k, "emoji": "📡", "count": v} for k, v in me_protos.items()]),
|
||||||
|
"alerts": alerts(me.get("alerts")),
|
||||||
|
"destinations": _dpi_donut(me_dests),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── overall (board) ──
|
||||||
|
all_cats: dict = {}
|
||||||
|
for d in devices:
|
||||||
|
for k, v in (d.get("by_category") or {}).items():
|
||||||
|
all_cats[k] = all_cats.get(k, 0) + v
|
||||||
|
all_stats = {
|
||||||
|
"devices": len(devices),
|
||||||
|
"flows": sum(int(d.get("flows", 0) or 0) for d in devices),
|
||||||
|
"alert_count": st.get("alert_count", 0),
|
||||||
|
"categories": cats(all_cats),
|
||||||
|
"protocols": _dpi_donut([{"label": p.get("name"), "emoji": "📡",
|
||||||
|
"count": int(p.get("bytes", 0) or 0)} for p in (st.get("top_protocols") or [])]),
|
||||||
|
"alerts": alerts(st.get("alerts")),
|
||||||
|
"destinations": _dpi_donut([{"label": a.get("name"), "emoji": "🌐",
|
||||||
|
"count": int(a.get("bytes", 0) or 0)} for a in (st.get("top_apps") or [])]),
|
||||||
|
}
|
||||||
|
return {"me": me_stats, "all": all_stats}
|
||||||
|
|
||||||
|
|
||||||
# NOTE: route order matters in FastAPI — specific routes (/report/me,
|
# NOTE: route order matters in FastAPI — specific routes (/report/me,
|
||||||
# /report/me/html) MUST be declared BEFORE the catch-all /report/{token},
|
# /report/me/html) MUST be declared BEFORE the catch-all /report/{token},
|
||||||
# otherwise FastAPI matches /report/me with token="me" and returns 404.
|
# otherwise FastAPI matches /report/me with token="me" and returns 404.
|
||||||
|
|
@ -2441,6 +2534,7 @@ async def report_me_html(request: Request) -> HTMLResponse:
|
||||||
cumulative=cumulative,
|
cumulative=cumulative,
|
||||||
graph=graph, graph_stats=gs, exposure_score=exposure_score,
|
graph=graph, graph_stats=gs, exposure_score=exposure_score,
|
||||||
charts=_build_report_charts(graph),
|
charts=_build_report_charts(graph),
|
||||||
|
dpi_exfil=_dpi_stats(mac_hash),
|
||||||
**session,
|
**session,
|
||||||
)
|
)
|
||||||
return HTMLResponse(html, headers={
|
return HTMLResponse(html, headers={
|
||||||
|
|
@ -2468,6 +2562,7 @@ async def report_me(request: Request) -> Response:
|
||||||
mac_hash = macmod.hash_mac(mac, salt)
|
mac_hash = macmod.hash_mac(mac, salt)
|
||||||
session = _aggregate_session(mac_hash)
|
session = _aggregate_session(mac_hash)
|
||||||
data = reports.build_report_data(mac_hash, session)
|
data = reports.build_report_data(mac_hash, session)
|
||||||
|
data["dpi_exfil"] = _dpi_stats(mac_hash) # #701 — DPI parity with the HTML report
|
||||||
pdf_bytes = reports.render_pdf(data)
|
pdf_bytes = reports.render_pdf(data)
|
||||||
fname = f"gondwana-toolbox-{mac_hash[:8]}.pdf"
|
fname = f"gondwana-toolbox-{mac_hash[:8]}.pdf"
|
||||||
return Response(
|
return Response(
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,40 @@ def render_pdf(report: dict) -> bytes:
|
||||||
_bullet(pdf, f"{a.get('emoji', '?')} {a.get('app', '?')} ({a.get('category', '?')}) - {a.get('count', 0)} connexions", font_size=8)
|
_bullet(pdf, f"{a.get('emoji', '?')} {a.get('app', '?')} ({a.get('category', '?')}) - {a.get('count', 0)} connexions", font_size=8)
|
||||||
pdf.ln(2)
|
pdf.ln(2)
|
||||||
|
|
||||||
|
# ── DPI / EXFILTRATION (R3 per-device + overall) — #701 (parity with HTML) ──
|
||||||
|
dexf = report.get("dpi_exfil") or {}
|
||||||
|
dme = dexf.get("me") or {}
|
||||||
|
dall = dexf.get("all") or {}
|
||||||
|
if dme.get("present") or dall.get("categories"):
|
||||||
|
_section(pdf, "DPI / EXFILTRATION (TUNNEL R3)")
|
||||||
|
|
||||||
|
def _donut_lines(title: str, items: list) -> None:
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
pdf.set_font(getattr(pdf, "_secubox_family", "Helvetica"), "B", 9)
|
||||||
|
pdf.cell(0, 5, _ascii_safe(title), ln=True)
|
||||||
|
for it in items:
|
||||||
|
_bullet(pdf, f"{it.get('emoji', '')} {it.get('label', '?')} - {it.get('pct', 0)}%", font_size=8)
|
||||||
|
|
||||||
|
if dme.get("present"):
|
||||||
|
up_mo = round((dme.get("up", 0) or 0) / 1048576, 1)
|
||||||
|
dn_mo = round((dme.get("down", 0) or 0) / 1048576, 1)
|
||||||
|
_kv(pdf, "Cet appareil",
|
||||||
|
f"{dme.get('flows', 0)} flux | {up_mo} Mo envoyes | {dn_mo} Mo recus | {dme.get('alert_count', 0)} alertes")
|
||||||
|
_donut_lines("Categories de service", dme.get("categories"))
|
||||||
|
_donut_lines("Protocoles", dme.get("protocols"))
|
||||||
|
_donut_lines("Alertes exfiltration", dme.get("alerts"))
|
||||||
|
_donut_lines("Top destinations (envoi)", dme.get("destinations"))
|
||||||
|
else:
|
||||||
|
_bullet(pdf, "Aucune donnee DPI pour cet appareil (surfer via le tunnel R3).", font_size=8)
|
||||||
|
|
||||||
|
if dall.get("categories"):
|
||||||
|
pdf.ln(1)
|
||||||
|
_kv(pdf, "Reseau (tous appareils)",
|
||||||
|
f"{dall.get('devices', 0)} appareils | {dall.get('flows', 0)} flux | {dall.get('alert_count', 0)} alertes")
|
||||||
|
_donut_lines("Categories (global)", dall.get("categories"))
|
||||||
|
pdf.ln(2)
|
||||||
|
|
||||||
# ── Geo top hosts (avec drapeaux + ASN) ──
|
# ── Geo top hosts (avec drapeaux + ASN) ──
|
||||||
geo_hosts = report.get("geo_top_hosts") or []
|
geo_hosts = report.get("geo_top_hosts") or []
|
||||||
if geo_hosts:
|
if geo_hosts:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user