Compare commits

...

4 Commits

Author SHA1 Message Date
CyberMind
433c5ca190
Merge pull request #573 from CyberMind-FR/feature/572-banner-colorful-emoji-chip-guirlande-fla
Some checks are pending
License Headers / check (push) Waiting to run
Banner: colourful emoji-chip guirlande (#572)
2026-06-14 11:02:14 +02:00
1a23c1f78a feat(toolbox): colourful emoji-chip guirlande banner (closes #572)
inject_banner right-side stats now render as vibrant rounded pills cycling
an 8-colour festive palette with neon box-shadow glow. Pure-ASCII inline
styling, works in CSP-strict + JS variants. secubox-toolbox 2.6.26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:01:37 +02:00
CyberMind
ff0503ea70
Merge pull request #571 from CyberMind-FR/feature/570-toolbox-mitm-dpi-media-type-statistifier
DPI media/content-type statistifier + donut (#570)
2026-06-14 10:58:08 +02:00
675f6ae458 feat(toolbox): DPI media/content-type statistifier + donut (closes #570)
mitmproxy_addons/media_stats.py buckets responses by content-type category
(emoji-iconified) + provider (eTLD+1), summing Content-Length (header only,
no body read). Rolling -> /run/secubox/media.json. api: /admin/media +
/admin/media/ui (SVG donut + emoji legend + top-5 providers w/ favicons).
Wired into the mitm-wg launcher. secubox-toolbox 2.6.25.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:57:48 +02:00
5 changed files with 231 additions and 2 deletions

View File

@ -1,3 +1,27 @@
secubox-toolbox (2.6.26-1~bookworm1) bookworm; urgency=medium
* Banner: colourful emoji-chip "guirlande" (#572). The right-side stat
row (status, flag, app, cookies, tracker-host, utiq, ghost, ASN) now
renders as vibrant rounded pills cycling an 8-colour festive palette
with a neon glow (box-shadow). Pure-ASCII inline styling — works in
both the CSP-strict and JS banner variants.
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 19:30:00 +0200
secubox-toolbox (2.6.25-1~bookworm1) bookworm; urgency=medium
* DPI media/content-type statistifier + donut (#570).
- mitmproxy_addons/media_stats.py : buckets every response by content-
type CATEGORY (page/image/video/audio/script/style/font/api/text/
other, emoji-iconified) and by PROVIDER (eTLD+1), summing
Content-Length (header only — never reads the body, safe on video).
Rolling counters → /run/secubox/media.json. Wired into the launcher.
- api: GET /admin/media (categories %+emoji, top-5 providers, totals) ;
GET /admin/media/ui (SVG donut + emoji legend + top-5 providers with
favicons).
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 19:00:00 +0200
secubox-toolbox (2.6.24-1~bookworm1) bookworm; urgency=medium secubox-toolbox (2.6.24-1~bookworm1) bookworm; urgency=medium
* webext: cap the popup top-tracker list to 5 items (#568); webext 0.1.3. * webext: cap the popup top-tracker list to 5 items (#568); webext 0.1.3.

View File

@ -487,7 +487,20 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
right_parts.append(f"&#x1F6E1; {g_blocked}&#xA0;&#x2715;&#xA0;~{g_kb}&#x202F;Ko") right_parts.append(f"&#x1F6E1; {g_blocked}&#xA0;&#x2715;&#xA0;~{g_kb}&#x202F;Ko")
if ctx["asn"]: if ctx["asn"]:
right_parts.append(_ncr(ctx["asn"])) right_parts.append(_ncr(ctx["asn"]))
right_text = " &#xB7; ".join(right_parts) # middle dot · = &#xB7; # #572 — render the stats as a colourful "guirlande" of emoji chips :
# each metric is a vibrant rounded pill with a neon glow, cycling
# through a festive palette. Pure-ASCII styling (NCR emojis) so the
# ascii-encode of both the CSP-strict + JS paths stays happy.
_GUIRLANDE = ("#c9a84c", "#00d4ff", "#00ff41", "#e63946",
"#9e76ff", "#ff9900", "#ff5a9e", "#39ff14")
_chips = []
for _i, _p in enumerate(right_parts):
_c = _GUIRLANDE[_i % len(_GUIRLANDE)]
_chips.append(
f"<span style=\"background:{_c};color:#0a0a0f;padding:1px 7px;"
f"margin:0 2px;border-radius:9px;font-weight:bold;white-space:nowrap;"
f"box-shadow:0 0 6px {_c},0 0 2px {_c}\">{_p}</span>")
right_text = "".join(_chips)
grade = ctx["grade"] grade = ctx["grade"]
grade_color = ctx["grade_color"] grade_color = ctx["grade_color"]
# Static emojis used in the left-side text # Static emojis used in the left-side text

View File

@ -0,0 +1,113 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# #570 — DPI media/content-type statistifier. Buckets every mitm response
# by content-type CATEGORY and by PROVIDER (eTLD+1), summing Content-Length
# (header only — never reads the body, safe on video/large media). Rolling
# in-memory counters flushed to /run/secubox/media.json for the donut UI.
from __future__ import annotations
import json
import os
import re
import time
from mitmproxy import http
_STATS = "/run/secubox/media.json"
# content-type → (category, emoji). Order matters (first match wins).
_CATS = (
("page", "\U0001F4C4", ("text/html", "application/xhtml")), # 📄
("image", "\U0001F5BC", ("image/",)), # 🖼
("video", "\U0001F3AC", ("video/", "application/vnd.apple.mpegurl",
"application/x-mpegurl", "application/dash+xml")), # 🎬
("audio", "\U0001F3B5", ("audio/",)), # 🎵
("script", "\U0001F9E9", ("javascript", "ecmascript")), # 🧩
("style", "\U0001F3A8", ("text/css",)), # 🎨
("font", "\U0001F524", ("font/", "application/font", "application/vnd.ms-fontobject",
"application/x-font")), # 🔤
("api", "\U0001F4E6", ("application/json", "+json", "application/xml",
"text/xml", "application/grpc")), # 📦
("text", "\U0001F4DD", ("text/plain", "text/")), # 📝
)
_OTHER = ("other", "") # ❓
EMOJI = {c: e for c, e, _ in _CATS}
EMOJI[_OTHER[0]] = _OTHER[1]
_MAX_PROVIDERS = 250
_2L_TLD = ("co.uk", "com.au", "co.jp", "co.nz", "com.br", "co.za", "gouv.fr")
_cats: dict = {}
_providers: dict = {}
_total = {"bytes": 0, "count": 0, "since": int(time.time())}
_last_flush = 0.0
def _category(ct: str) -> tuple:
ct = (ct or "").split(";", 1)[0].strip().lower()
if not ct:
return _OTHER
for cat, emoji, frags in _CATS:
if any(f in ct for f in frags):
return (cat, emoji)
return _OTHER
def _registrable(host: str) -> str:
host = (host or "").split(":", 1)[0].lower().strip(".")
if not host or host.replace(".", "").isdigit():
return host or "?"
parts = host.split(".")
if len(parts) <= 2:
return host
last2 = ".".join(parts[-2:])
if last2 in _2L_TLD and len(parts) >= 3:
return ".".join(parts[-3:])
return last2
def _flush(force: bool = False) -> None:
global _last_flush
now = time.time()
if not force and (now - _last_flush) < 5:
return
_last_flush = now
# cap providers to the heaviest _MAX_PROVIDERS by bytes
global _providers
if len(_providers) > _MAX_PROVIDERS:
_providers = dict(sorted(_providers.items(),
key=lambda kv: -kv[1]["bytes"])[:_MAX_PROVIDERS])
try:
os.makedirs(os.path.dirname(_STATS), exist_ok=True)
with open(_STATS, "w", encoding="utf-8") as f:
json.dump({"categories": _cats, "providers": _providers,
"total": _total, "updated": int(now)}, f)
except Exception:
pass
class MediaStats:
def response(self, flow: http.HTTPFlow) -> None:
if not flow.response:
return
h = flow.response.headers
cat, _ = _category(h.get("content-type", ""))
try:
size = int(h.get("content-length", "0") or "0")
except (TypeError, ValueError):
size = 0
prov = _registrable(flow.request.pretty_host or "")
c = _cats.setdefault(cat, {"bytes": 0, "count": 0})
c["bytes"] += size
c["count"] += 1
p = _providers.setdefault(prov, {"bytes": 0, "count": 0})
p["bytes"] += size
p["count"] += 1
_total["bytes"] += size
_total["count"] += 1
_flush()
addons = [MediaStats()]

View File

@ -110,7 +110,7 @@ fi
# ad_ghost (#566) runs right after protective_mode: for R3+/R4 it 204s known # ad_ghost (#566) runs right after protective_mode: for R3+/R4 it 204s known
# ad/tracker hosts (bandwidth save) at request time and injects ad-hiding CSS # ad/tracker hosts (bandwidth save) at request time and injects ad-hiding CSS
# on HTML responses. Gated by the modular filter config (toolbox WebUI). # on HTML responses. Gated by the modular filter config (toolbox WebUI).
for addon in inject_xff utiq_defense protective_mode ad_ghost local_store social_graph inject_banner dpi cookies avatar ja4 soc_relay cert_pin_detect; do for addon in inject_xff utiq_defense protective_mode ad_ghost local_store social_graph inject_banner dpi cookies avatar ja4 soc_relay cert_pin_detect media_stats; do
ARGS+=(-s "$ADDON_DIR/${addon}.py") ARGS+=(-s "$ADDON_DIR/${addon}.py")
done done

View File

@ -2447,6 +2447,85 @@ async def admin_ghost() -> dict:
return out return out
_MEDIA_EMOJI = {
"page": "\U0001F4C4", "image": "\U0001F5BC", "video": "\U0001F3AC",
"audio": "\U0001F3B5", "script": "\U0001F9E9", "style": "\U0001F3A8",
"font": "\U0001F524", "api": "\U0001F4E6", "text": "\U0001F4DD",
"other": "",
}
@router.get("/admin/media")
async def admin_media() -> dict:
"""#570 — DPI media/content-type statistics for the donut UI."""
import json as _json
from pathlib import Path as _P
raw: dict = {"categories": {}, "providers": {}, "total": {"bytes": 0, "count": 0}}
try:
st = _P("/run/secubox/media.json")
if st.exists():
raw.update(_json.loads(st.read_text()))
except Exception:
pass
tot_b = max(int(raw.get("total", {}).get("bytes", 0)), 1)
cats = []
for cat, v in (raw.get("categories") or {}).items():
b = int(v.get("bytes", 0))
cats.append({"cat": cat, "emoji": _MEDIA_EMOJI.get(cat, ""),
"bytes": b, "count": int(v.get("count", 0)),
"mb": round(b / 1048576, 1), "pct": round(100 * b / tot_b, 1)})
cats.sort(key=lambda x: -x["bytes"])
provs = sorted(
({"provider": p, "bytes": int(v.get("bytes", 0)),
"count": int(v.get("count", 0)), "mb": round(int(v.get("bytes", 0)) / 1048576, 1)}
for p, v in (raw.get("providers") or {}).items()),
key=lambda x: -x["bytes"])[:5]
return {"categories": cats, "top_providers": provs,
"total_mb": round(raw.get("total", {}).get("bytes", 0) / 1048576, 1),
"total_count": raw.get("total", {}).get("count", 0),
"updated": raw.get("updated")}
@router.get("/admin/media/ui", response_class=HTMLResponse)
async def admin_media_ui() -> HTMLResponse:
"""#570 — donut + emoji legend + top-5 providers."""
html = """<!doctype html><html lang=fr><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>DPI Médias ToolBoX</title>
<style>
body{background:#0a0a0f;color:#e8e6d9;font:14px system-ui,sans-serif;max-width:560px;margin:24px auto;padding:0 18px;text-align:center}
h1{color:#c9a84c;font-size:18px} .muted{color:#6b6b7a;font-size:12px}
#legend{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:10px 0}
.lg{background:#12121a;border:1px solid #222;border-radius:14px;padding:4px 9px;font-size:12px}
table{width:100%;border-collapse:collapse;margin-top:14px;font-size:13px}
td{padding:5px 4px;border-bottom:1px solid #1a1a22;text-align:left} td.r{text-align:right;color:#6b6b7a}
.fav{width:16px;height:16px;border-radius:3px;vertical-align:middle;margin-right:6px;background:#1a1a22}
h2{color:#6e40c9;font-size:13px;margin:18px 0 4px;text-align:left}
</style>
<h1>📊 DPI types de contenus</h1>
<p class=muted id=tot></p>
<svg id=donut viewBox="0 0 200 200" width=200 height=200></svg>
<div id=legend></div>
<h2>Top 5 fournisseurs</h2>
<table id=provs></table>
<script>
const PAL=['#c9a84c','#00d4ff','#e63946','#6e40c9','#00ff41','#ff9900','#9aa0a6','#ff5a9e','#4285f4','#888'];
const SVGNS='http://www.w3.org/2000/svg';
function arc(cx,cy,r,a0,a1){const p=(a,rr)=>[cx+rr*Math.cos(a),cy+rr*Math.sin(a)];const[x0,y0]=p(a0,r),[x1,y1]=p(a1,r),[xi1,yi1]=p(a1,r*0.58),[xi0,yi0]=p(a0,r*0.58);const big=a1-a0>Math.PI?1:0;return`M${x0} ${y0}A${r} ${r} 0 ${big} 1 ${x1} ${y1}L${xi1} ${yi1}A${r*0.58} ${r*0.58} 0 ${big} 0 ${xi0} ${yi0}Z`;}
fetch('/admin/media').then(r=>r.json()).then(d=>{
document.getElementById('tot').textContent=`${d.total_mb||0} Mo · ${d.total_count||0} flux`;
const svg=document.getElementById('donut');const cats=d.categories||[];
let a=-Math.PI/2;const tot=cats.reduce((s,c)=>s+c.bytes,0)||1;
cats.forEach((c,i)=>{const a1=a+2*Math.PI*c.bytes/tot;const path=document.createElementNS(SVGNS,'path');path.setAttribute('d',arc(100,100,92,a,a1));path.setAttribute('fill',PAL[i%PAL.length]);const t=document.createElementNS(SVGNS,'title');t.textContent=`${c.emoji} ${c.cat} ${c.pct}% (${c.mb} Mo)`;path.appendChild(t);svg.appendChild(path);a=a1;});
const lg=document.getElementById('legend');
cats.forEach((c,i)=>{const s=document.createElement('span');s.className='lg';s.innerHTML=`<b style="color:${PAL[i%PAL.length]}"></b> ${c.emoji} ${c.cat} ${c.pct}%`;lg.appendChild(s);});
const tb=document.getElementById('provs');
(d.top_providers||[]).forEach(p=>{const tr=document.createElement('tr');tr.innerHTML=`<td><img class=fav loading=lazy src="/social/favicon/${encodeURIComponent(p.provider)}" onerror="this.style.visibility='hidden'">${p.provider}</td><td class=r>${p.mb} Mo · ${p.count}</td>`;tb.appendChild(tr);});
});
</script></html>"""
return HTMLResponse(content=html)
@router.get("/admin/filters") @router.get("/admin/filters")
async def admin_filters_get() -> dict: async def admin_filters_get() -> dict:
"""#566 — modular mitm filter config (read).""" """#566 — modular mitm filter config (read)."""