mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 14:10:44 +00:00
Compare commits
4 Commits
b689c235f6
...
433c5ca190
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
433c5ca190 | ||
| 1a23c1f78a | |||
|
|
ff0503ea70 | ||
| 675f6ae458 |
|
|
@ -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
|
||||
|
||||
* webext: cap the popup top-tracker list to 5 items (#568); webext 0.1.3.
|
||||
|
|
|
|||
|
|
@ -487,7 +487,20 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
|||
right_parts.append(f"🛡 {g_blocked} ✕ ~{g_kb} Ko")
|
||||
if ctx["asn"]:
|
||||
right_parts.append(_ncr(ctx["asn"]))
|
||||
right_text = " · ".join(right_parts) # middle dot · = ·
|
||||
# #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_color = ctx["grade_color"]
|
||||
# Static emojis used in the left-side text
|
||||
|
|
|
|||
113
packages/secubox-toolbox/mitmproxy_addons/media_stats.py
Normal file
113
packages/secubox-toolbox/mitmproxy_addons/media_stats.py
Normal 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()]
|
||||
|
|
@ -110,7 +110,7 @@ fi
|
|||
# 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
|
||||
# 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")
|
||||
done
|
||||
|
||||
|
|
|
|||
|
|
@ -2447,6 +2447,85 @@ async def admin_ghost() -> dict:
|
|||
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")
|
||||
async def admin_filters_get() -> dict:
|
||||
"""#566 — modular mitm filter config (read)."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user