Compare commits

..

No commits in common. "f5da2f6aa871c1704a6287acc49697322a2dec44" and "434d3aba6a2dbc63df34af7aa5005f0238fcc8e8" have entirely different histories.

3 changed files with 0 additions and 230 deletions

View File

@ -71,13 +71,6 @@ 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}
.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}
/* ── 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>
<body>
@ -96,38 +89,9 @@ 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 _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>
<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')) %}
<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> —
@ -338,57 +302,6 @@ ul{list-style:none;padding-left:.2rem}li{padding:.12rem 0;font-size:.82rem}li::b
</div>
</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">
<a href="/report/me?mh={{ mac_hash }}">⬇ Télécharger le PDF</a>
<a href="/social/me?mh={{ mac_hash }}">🕸️ Ma carto</a>
@ -409,18 +322,4 @@ 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>
</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>

View File

@ -2386,99 +2386,6 @@ def _build_report_charts(graph: dict) -> dict:
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,
# /report/me/html) MUST be declared BEFORE the catch-all /report/{token},
# otherwise FastAPI matches /report/me with token="me" and returns 404.
@ -2534,7 +2441,6 @@ async def report_me_html(request: Request) -> HTMLResponse:
cumulative=cumulative,
graph=graph, graph_stats=gs, exposure_score=exposure_score,
charts=_build_report_charts(graph),
dpi_exfil=_dpi_stats(mac_hash),
**session,
)
return HTMLResponse(html, headers={
@ -2562,7 +2468,6 @@ async def report_me(request: Request) -> Response:
mac_hash = macmod.hash_mac(mac, salt)
session = _aggregate_session(mac_hash)
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)
fname = f"gondwana-toolbox-{mac_hash[:8]}.pdf"
return Response(

View File

@ -218,40 +218,6 @@ 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)
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_hosts = report.get("geo_top_hosts") or []
if geo_hosts: