mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-02 01:52:06 +00:00
Compare commits
18 Commits
18f727b7d7
...
c043c5fca8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c043c5fca8 | ||
| 10d22f05b7 | |||
|
|
5ece3f1208 | ||
| 5ca4ecf455 | |||
|
|
ace77976fa | ||
| 4ffd66bb2d | |||
|
|
9b8144073f | ||
| 2f04b0fa84 | |||
|
|
270f655d3a | ||
| b366946855 | |||
|
|
433c5ca190 | ||
| 1a23c1f78a | |||
|
|
ff0503ea70 | ||
| 675f6ae458 | |||
|
|
b689c235f6 | ||
| d26992d905 | |||
|
|
295f77601d | ||
| f176fa3173 |
|
|
@ -31,7 +31,7 @@ to your cabine over the R3 tunnel — no third-party calls.
|
||||||
Published release `.xpi` (downloadable directly):
|
Published release `.xpi` (downloadable directly):
|
||||||
|
|
||||||
```
|
```
|
||||||
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.2/secubox-toolbox-webext.xpi
|
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi
|
||||||
```
|
```
|
||||||
|
|
||||||
The toolbox also serves it from the cabine:
|
The toolbox also serves it from the cabine:
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,29 @@ async function wipe(host, token) {
|
||||||
return await resp.json();
|
return await resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #574 — protection stats + modular filter toggles (cabine admin API).
|
||||||
|
async function ghost(host) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${baseUrl(host)}/admin/ghost`, { credentials: "omit" });
|
||||||
|
return r.ok ? await r.json() : null;
|
||||||
|
} catch (_) { return null; }
|
||||||
|
}
|
||||||
|
async function getAdminFilters(host) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${baseUrl(host)}/admin/filters`, { credentials: "omit" });
|
||||||
|
return r.ok ? await r.json() : null;
|
||||||
|
} catch (_) { return null; }
|
||||||
|
}
|
||||||
|
async function setAdminFilters(host, patch) {
|
||||||
|
const r = await fetch(`${baseUrl(host)}/admin/filters`, {
|
||||||
|
method: "POST", credentials: "omit",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
return await r.json();
|
||||||
|
}
|
||||||
|
|
||||||
// Favicon of a major site/tracker via the cabine's server-side proxy
|
// Favicon of a major site/tracker via the cabine's server-side proxy
|
||||||
// (7-day cached PNG, transparent 1×1 fallback) — no third-party call.
|
// (7-day cached PNG, transparent 1×1 fallback) — no third-party call.
|
||||||
function faviconUrl(host, domain) {
|
function faviconUrl(host, domain) {
|
||||||
|
|
@ -112,6 +135,9 @@ const SbxApi = {
|
||||||
r3Check,
|
r3Check,
|
||||||
graph,
|
graph,
|
||||||
wipe,
|
wipe,
|
||||||
|
ghost,
|
||||||
|
getAdminFilters,
|
||||||
|
setAdminFilters,
|
||||||
faviconUrl,
|
faviconUrl,
|
||||||
socialUrl,
|
socialUrl,
|
||||||
reportUrl,
|
reportUrl,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DEFAULT_HOST="kbin.gk2.secubox.in"
|
DEFAULT_HOST="kbin.gk2.secubox.in"
|
||||||
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.2/secubox-toolbox-webext.xpi"
|
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi"
|
||||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
say(){ printf '\033[1;36m▸\033[0m %s\n' "$*"; }
|
say(){ printf '\033[1;36m▸\033[0m %s\n' "$*"; }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "SecuBox ToolBoX — Cartographie sociale",
|
"name": "SecuBox ToolBoX — Cartographie sociale",
|
||||||
"version": "0.1.2",
|
"version": "0.1.4",
|
||||||
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
|
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,14 @@ button.danger { color: var(--cinnabar); border-color: var(--cinnabar); }
|
||||||
.row .fav { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; background: #1a1a22; object-fit: contain; }
|
.row .fav { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; background: #1a1a22; object-fit: contain; }
|
||||||
.row .dom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.row .dom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.row .hits { color: var(--muted); }
|
.row .hits { color: var(--muted); }
|
||||||
|
|
||||||
|
/* #574 — protection panel */
|
||||||
|
#protect { margin: 8px 0; padding: 8px; background: #0e0e15; border: 1px solid #222; border-radius: 8px; }
|
||||||
|
.phead { color: var(--matrix); font-weight: 700; font-size: 12px; margin-bottom: 6px; }
|
||||||
|
.gstat { color: var(--muted); font-weight: 400; font-size: 10px; }
|
||||||
|
.tg { display: flex; align-items: center; gap: 6px; font-size: 11px; padding: 3px 0; }
|
||||||
|
.tg select { margin-left: auto; background: #14141c; color: var(--text); border: 1px solid #333; border-radius: 4px; }
|
||||||
|
#protect input { accent-color: var(--void); }
|
||||||
.tier { font-size: 9px; padding: 1px 4px; border-radius: 3px; }
|
.tier { font-size: 9px; padding: 1px 4px; border-radius: 3px; }
|
||||||
.tier.cdn { background: #1d2a33; color: var(--cyan); }
|
.tier.cdn { background: #1d2a33; color: var(--cyan); }
|
||||||
.tier.ab { background: #2a1416; color: var(--cinnabar); }
|
.tier.ab { background: #2a1416; color: var(--cinnabar); }
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,17 @@
|
||||||
|
|
||||||
<div class="toplist" id="topList"></div>
|
<div class="toplist" id="topList"></div>
|
||||||
|
|
||||||
|
<section id="protect">
|
||||||
|
<div class="phead">🛡 Protection <span id="ghostStat" class="gstat"></span></div>
|
||||||
|
<label class="tg"><input type="checkbox" data-f="ad_ghost"> Masquer pubs/bannières (R3+)</label>
|
||||||
|
<label class="tg"><input type="checkbox" data-f="ad_ghost_block"> Bloquer hôtes pub (économie)</label>
|
||||||
|
<label class="tg"><input type="checkbox" data-f="banner"> Bannière transparence</label>
|
||||||
|
<label class="tg">Mode protecteur
|
||||||
|
<select data-f="protective"><option value="off">off</option><option value="alert">alert</option><option value="spoof">spoof</option></select>
|
||||||
|
</label>
|
||||||
|
<p id="protectMsg" class="muted"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="openFull">🗺️ Cartographie complète</button>
|
<button id="openFull">🗺️ Cartographie complète</button>
|
||||||
<button id="pdf">📄 Rapport PDF</button>
|
<button id="pdf">📄 Rapport PDF</button>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function fillTopList(nodes) {
|
||||||
(nodes || [])
|
(nodes || [])
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
|
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
|
||||||
.slice(0, 12)
|
.slice(0, 5)
|
||||||
.forEach((n) => {
|
.forEach((n) => {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "row";
|
row.className = "row";
|
||||||
|
|
@ -64,6 +64,41 @@ function paint(data) {
|
||||||
fillTopList(data.nodes);
|
fillTopList(data.nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #574 — protection stats + live filter toggles in the popup.
|
||||||
|
async function loadProtection() {
|
||||||
|
const sec = $("protect");
|
||||||
|
if (!sec) return;
|
||||||
|
const g = await api.ghost(curHost);
|
||||||
|
if (g) {
|
||||||
|
$("ghostStat").textContent =
|
||||||
|
`${g.blocked_requests || 0} bloqués · ~${g.mb_saved_est || 0} Mo · ${g.pages_cleaned || 0} nettoyées`;
|
||||||
|
}
|
||||||
|
const f = await api.getAdminFilters(curHost);
|
||||||
|
if (!f) { sec.style.opacity = "0.5"; return; }
|
||||||
|
sec.style.opacity = "1";
|
||||||
|
sec.querySelectorAll("[data-f]").forEach((el) => {
|
||||||
|
const k = el.dataset.f;
|
||||||
|
if (el.type === "checkbox") el.checked = !!f[k];
|
||||||
|
else el.value = f[k];
|
||||||
|
});
|
||||||
|
if (!sec.dataset.wired) {
|
||||||
|
sec.dataset.wired = "1";
|
||||||
|
sec.querySelectorAll("[data-f]").forEach((el) => {
|
||||||
|
el.addEventListener("change", async () => {
|
||||||
|
const v = el.type === "checkbox" ? el.checked : el.value;
|
||||||
|
try {
|
||||||
|
await api.setAdminFilters(curHost, { [el.dataset.f]: v });
|
||||||
|
$("protectMsg").textContent = "✓ appliqué";
|
||||||
|
setTimeout(() => ($("protectMsg").textContent = ""), 1000);
|
||||||
|
loadProtection();
|
||||||
|
} catch (e) {
|
||||||
|
$("protectMsg").textContent = "erreur : " + e.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const cfg = await api.getConfig();
|
const cfg = await api.getConfig();
|
||||||
curHost = cfg.host || api.DEFAULTS.host;
|
curHost = cfg.host || api.DEFAULTS.host;
|
||||||
|
|
@ -87,6 +122,7 @@ async function load() {
|
||||||
const data = await api.graph(cfg.host, cfg.token, cfg.since);
|
const data = await api.graph(cfg.host, cfg.token, cfg.since);
|
||||||
paint(data);
|
paint(data);
|
||||||
$("liveMsg").textContent = "";
|
$("liveMsg").textContent = "";
|
||||||
|
loadProtection();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (String(e.message) === "token-expired") {
|
if (String(e.message) === "token-expired") {
|
||||||
// token died — drop it and go back to pairing
|
// token died — drop it and go back to pairing
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,106 @@
|
||||||
|
secubox-toolbox (2.6.31-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* ad_ghost: remove ad placeholders entirely (#584, reverses #576). Ghosted
|
||||||
|
ad slots now collapse with display:none — no black-hole/void, the space
|
||||||
|
disappears. Host-blocking (204) still saves the bandwidth.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 14 Jun 2026 14:00:00 +0200
|
||||||
|
|
||||||
|
secubox-toolbox (2.6.30-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* ad_ghost: ghosted ads become a CSS "black hole" placeholder (#576).
|
||||||
|
Instead of display:none (a collapsed blank gap), the ghosted ad slot is
|
||||||
|
a layered dark void — radial-gradient background + inset glow + a glowing
|
||||||
|
accretion-disc via ::after, real content hidden. Intentional reclaimed
|
||||||
|
space, R3+/R4, toggle via the ad_ghost filter.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 14 Jun 2026 13:30:00 +0200
|
||||||
|
|
||||||
|
secubox-toolbox (2.6.29-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* fix(postinst): portal reliably restarts after dpkg upgrade (#581). The
|
||||||
|
upgrade SIGTERMs the units before postinst runs, so the old try-restart
|
||||||
|
loop was a no-op on the already-stopped portal → secubox-toolbox.service
|
||||||
|
stayed dead → kbin 503 (hit twice 2026-06-14). Now ENABLED units get a
|
||||||
|
full `restart` (starts even if stopped); others keep `try-restart`.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 14 Jun 2026 13:00:00 +0200
|
||||||
|
|
||||||
|
secubox-toolbox (2.6.28-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Eye graph: hide all IP nodes + cap clients list (#575).
|
||||||
|
- social.js: drop every IP-only tracker node (IPv4/IPv6) from the
|
||||||
|
eye force-graph — no more domain+IP double bubbles for one tracker.
|
||||||
|
Remaining tracker nodes are labelled country-flag + domain name.
|
||||||
|
- www/toolbox index.html #clients tab: show the top 5 clients only
|
||||||
|
("+ N autres" note when more).
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 14 Jun 2026 12:30:00 +0200
|
||||||
|
|
||||||
|
secubox-toolbox (2.6.27-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* webext popup: protection stats + live filter toggles (#574); webext
|
||||||
|
0.1.4. New 🛡 Protection panel — ghost savings (blocked / ~Mo / pages
|
||||||
|
cleaned via /admin/ghost) + live toggles for ad_ghost / ad_ghost_block /
|
||||||
|
banner / protective(off|alert|spoof) via /admin/filters. Top-tracker
|
||||||
|
list stays top-5. /wg/toolbox.xpi tag-pin → webext-v0.1.4.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sun, 14 Jun 2026 12:00:00 +0200
|
||||||
|
|
||||||
|
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.
|
||||||
|
/wg/toolbox.xpi tag-pin → webext-v0.1.4.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 18:30:00 +0200
|
||||||
|
|
||||||
|
secubox-toolbox (2.6.23-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Modular mitm filters + R3+/R4 silent ad/banner ghoster (#566).
|
||||||
|
- secubox_toolbox/filters.py : single filter config
|
||||||
|
(/etc/secubox/toolbox/filters.json), 5 s cached, toggled live from
|
||||||
|
the toolbox WebUI — banner / protective(off|alert|spoof) / ad_ghost /
|
||||||
|
ad_ghost_block / per-category cosmetics. No restart to flip.
|
||||||
|
- mitmproxy_addons/ad_ghost.py : for R3+/R4 (10.99.1.0/24) only —
|
||||||
|
204 known ad/tracker hosts (real bandwidth save) + inject ad-hiding
|
||||||
|
CSS (ads / consent-nag / newsletter / social-widget categories) into
|
||||||
|
HTML. Tallies blocked_requests + bytes_saved_est + pages_cleaned →
|
||||||
|
/run/secubox/ghost.json. Wired into the mitm-wg launcher.
|
||||||
|
- inject_banner : reads the filter (banner on/off), shows the ghost
|
||||||
|
quick-stats (shield N + ~X Ko), relabels status inspected→protected
|
||||||
|
on R3+/R4. protective_mode level now sourced from the filter config
|
||||||
|
(env fallback).
|
||||||
|
- api: GET/POST /admin/filters, GET /admin/ghost, GET /admin/filters/ui
|
||||||
|
(toggle panel). postinst seeds filters.json (preserved on upgrade).
|
||||||
|
Doctrine: opt-in via filter, logged, reversible ; cosmetic ghosting only
|
||||||
|
on the opted-in R3+/R4 tiers, 1st-party pages stay usable.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 18:00:00 +0200
|
||||||
|
|
||||||
secubox-toolbox (2.6.22-1~bookworm1) bookworm; urgency=medium
|
secubox-toolbox (2.6.22-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* detect_antibot: deployment vs challenge, response-level signals (#564,
|
* detect_antibot: deployment vs challenge, response-level signals (#564,
|
||||||
|
|
@ -57,7 +160,7 @@ secubox-toolbox (2.6.19-1~bookworm1) bookworm; urgency=medium
|
||||||
transparent 1×1 fallback → the tier-coloured circle shows through),
|
transparent 1×1 fallback → the tier-coloured circle shows through),
|
||||||
clipped to the bubble. No IP/ASN displayed anywhere.
|
clipped to the bubble. No IP/ASN displayed anywhere.
|
||||||
- Companion webext popup gains favicons in its top-tracker list
|
- Companion webext popup gains favicons in its top-tracker list
|
||||||
(clients/webext-toolbox 0.1.2). /wg/toolbox.xpi tag-pin → webext-v0.1.2.
|
(clients/webext-toolbox 0.1.2). /wg/toolbox.xpi tag-pin → webext-v0.1.4.
|
||||||
|
|
||||||
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 15:30:00 +0200
|
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 15:30:00 +0200
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,23 @@ case "$1" in
|
||||||
chown root:secubox-toolbox /etc/secubox/toolbox/ca/ca.pem /etc/secubox/toolbox/ca/key.pem 2>/dev/null || true
|
chown root:secubox-toolbox /etc/secubox/toolbox/ca/ca.pem /etc/secubox/toolbox/ca/key.pem 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# #566 : modular filter config (toolbox WebUI toggles). Seed once;
|
||||||
|
# preserve operator edits on upgrade. Writable by the portal user.
|
||||||
|
install -d -m 0750 -o root -g secubox-toolbox /etc/secubox/toolbox 2>/dev/null || true
|
||||||
|
if [ ! -f /etc/secubox/toolbox/filters.json ]; then
|
||||||
|
cat > /etc/secubox/toolbox/filters.json <<'SBXFILTERS'
|
||||||
|
{
|
||||||
|
"banner": true,
|
||||||
|
"protective": "spoof",
|
||||||
|
"ad_ghost": true,
|
||||||
|
"ad_ghost_block": true,
|
||||||
|
"ad_ghost_categories": {"ads": true, "consent_nag": true, "newsletter": true, "social_widgets": true}
|
||||||
|
}
|
||||||
|
SBXFILTERS
|
||||||
|
chown root:secubox-toolbox /etc/secubox/toolbox/filters.json
|
||||||
|
chmod 0664 /etc/secubox/toolbox/filters.json
|
||||||
|
fi
|
||||||
|
|
||||||
# 4. Storage dir (SQLite + future PDF reports)
|
# 4. Storage dir (SQLite + future PDF reports)
|
||||||
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox
|
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox
|
||||||
# #536 : Android APK serve dir + best-effort fetch of the latest
|
# #536 : Android APK serve dir + best-effort fetch of the latest
|
||||||
|
|
@ -214,14 +231,15 @@ fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Phase 10 (#501 perf) : on UPGRADE ($2 = previous version), try-restart
|
# Phase 10 (#501 perf) : on UPGRADE ($2 = previous version), bring the
|
||||||
# the long-running daemons so the new code is live without operator
|
# long-running daemons back on the new code without operator action.
|
||||||
# intervention. dh_installsystemd ships --no-start --no-enable in
|
# dh_installsystemd ships --no-start --no-enable in debian/rules.
|
||||||
# debian/rules so without this loop secubox-toolbox.service stays dead
|
# #581 fix : the dpkg upgrade SIGTERMs the units BEFORE this postinst
|
||||||
# post-upgrade until reboot (caught 2026-06-09 : kbin.gk2 503'd for 5 min
|
# runs, so `try-restart` (a no-op on a stopped unit) left
|
||||||
# after the 2.5.0 → 2.5.1 upgrade because the unit was SIGTERMed and
|
# secubox-toolbox.service DEAD post-upgrade -> kbin 503 (caught twice on
|
||||||
# never restarted). try-restart is a no-op when the unit is not
|
# 2026-06-14, first on 2026-06-09). So : ENABLED units get a full
|
||||||
# running, so it's safe on fresh install / unconfigured boards.
|
# `restart` (starts them even if currently stopped) ; everything else
|
||||||
|
# keeps `try-restart` (preserve an operator's deliberate stop).
|
||||||
if [ -n "${2:-}" ] && [ -d /run/systemd/system ]; then
|
if [ -n "${2:-}" ] && [ -d /run/systemd/system ]; then
|
||||||
for unit in secubox-toolbox.service \
|
for unit in secubox-toolbox.service \
|
||||||
secubox-toolbox-mitm.service \
|
secubox-toolbox-mitm.service \
|
||||||
|
|
@ -229,7 +247,11 @@ fi
|
||||||
secubox-toolbox-mitm-wg-worker@2.service \
|
secubox-toolbox-mitm-wg-worker@2.service \
|
||||||
secubox-toolbox-mitm-wg-worker@3.service \
|
secubox-toolbox-mitm-wg-worker@3.service \
|
||||||
secubox-toolbox-mitm-wg-worker@4.service ; do
|
secubox-toolbox-mitm-wg-worker@4.service ; do
|
||||||
systemctl try-restart "$unit" 2>/dev/null || true
|
if systemctl is-enabled --quiet "$unit" 2>/dev/null; then
|
||||||
|
systemctl restart "$unit" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
systemctl try-restart "$unit" 2>/dev/null || true
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
161
packages/secubox-toolbox/mitmproxy_addons/ad_ghost.py
Normal file
161
packages/secubox-toolbox/mitmproxy_addons/ad_ghost.py
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
#
|
||||||
|
# #566 — R3+/R4 silent ad/banner/widget GHOSTER + economisable savings.
|
||||||
|
#
|
||||||
|
# For R3+/R4 tunnel clients (on 10.99.1.0/24) ONLY, and only when enabled
|
||||||
|
# in the modular filter config (toolbox WebUI → filters.json):
|
||||||
|
# - cosmetic ghost: inject a <style> that hides ad / consent-nag /
|
||||||
|
# newsletter-popup / social-widget containers (1st-party page stays
|
||||||
|
# usable) ;
|
||||||
|
# - block: 204 known ad/tracker hosts to save real bandwidth.
|
||||||
|
# Tallies ghosted requests + estimated bytes saved → /run/secubox/ghost.json,
|
||||||
|
# which inject_banner surfaces as quick stats. Doctrine: opt-in (filter
|
||||||
|
# toggle), logged, reversible.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from mitmproxy import http
|
||||||
|
|
||||||
|
# Shared modular filter config (best-effort import; safe defaults if absent).
|
||||||
|
try:
|
||||||
|
if "/usr/lib/secubox/toolbox" not in sys.path:
|
||||||
|
sys.path.insert(0, "/usr/lib/secubox/toolbox")
|
||||||
|
from secubox_toolbox.filters import get_filters
|
||||||
|
except Exception:
|
||||||
|
def get_filters(force: bool = False):
|
||||||
|
return {"ad_ghost": True, "ad_ghost_block": True,
|
||||||
|
"ad_ghost_categories": {"ads": True, "consent_nag": True,
|
||||||
|
"newsletter": True, "social_widgets": True}}
|
||||||
|
|
||||||
|
_STATS = "/run/secubox/ghost.json"
|
||||||
|
_EST_BYTES_PER_REQ = 45000 # honest estimate per blocked ad/tracker request
|
||||||
|
|
||||||
|
# Ad / tracker hosts to 204 (bandwidth save). Conservative: ad/tracker only.
|
||||||
|
_AD_HOST = re.compile(
|
||||||
|
r"(?:^|\.)(?:doubleclick|googlesyndication|googleadservices|"
|
||||||
|
r"googletagservices|adservice\.google|amazon-adsystem|adnxs|adsrvr|"
|
||||||
|
r"adform|criteo|rubiconproject|taboola|outbrain|smartadserver|moatads|"
|
||||||
|
r"scorecardresearch|2mdn|adroll|pubmatic|openx|casalemedia|"
|
||||||
|
r"yieldlove|sharethrough|teads|3lift|adsystem|adserver)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cosmetic hide selectors, grouped so the WebUI can toggle each category.
|
||||||
|
_COSMETIC = {
|
||||||
|
"ads": (
|
||||||
|
'[id^="google_ads"]', '[id^="div-gpt-ad"]', 'ins.adsbygoogle',
|
||||||
|
'iframe[src*="doubleclick"]', 'iframe[src*="googlesyndication"]',
|
||||||
|
'iframe[src*="amazon-adsystem"]', '[class*="ad-banner"]',
|
||||||
|
'[class*="advert"]', '[id*="banner-ad"]', '[id*="ad-container"]',
|
||||||
|
'[class*="-ads"]', '[class*="sponsored"]', 'aside[aria-label*="publicit"]',
|
||||||
|
),
|
||||||
|
"consent_nag": (
|
||||||
|
'#onetrust-banner-sdk', '#onetrust-consent-sdk', '#didomi-host',
|
||||||
|
'.qc-cmp2-container', '[id^="sp_message_container"]',
|
||||||
|
'[id*="cookie-consent"]', '[class*="cookie-banner"]',
|
||||||
|
'[class*="cookie-notice"]', '[aria-label*="cookie"]', '.cmpbox',
|
||||||
|
),
|
||||||
|
"newsletter": (
|
||||||
|
'[class*="newsletter-popup"]', '[class*="signup-modal"]',
|
||||||
|
'[id*="newsletter-modal"]', '[class*="subscribe-overlay"]',
|
||||||
|
),
|
||||||
|
"social_widgets": (
|
||||||
|
'.fb-like', '.twitter-share-button', '[class*="social-share"]',
|
||||||
|
'iframe[src*="facebook.com/plugins"]', 'iframe[src*="platform.twitter"]',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
_RE_HEAD = re.compile(rb"</head>", re.IGNORECASE)
|
||||||
|
_MARK = b"sbx-ghost-style"
|
||||||
|
|
||||||
|
_counts = {"blocked_requests": 0, "bytes_saved_est": 0, "pages_cleaned": 0,
|
||||||
|
"since": int(time.time())}
|
||||||
|
_last_flush = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _is_r3plus(flow) -> bool:
|
||||||
|
try:
|
||||||
|
ip = flow.client_conn.peername[0]
|
||||||
|
return bool(ip) and ip.startswith("10.99.1.")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _flush(force: bool = False) -> None:
|
||||||
|
global _last_flush
|
||||||
|
now = time.time()
|
||||||
|
if not force and (now - _last_flush) < 5:
|
||||||
|
return
|
||||||
|
_last_flush = now
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(_STATS), exist_ok=True)
|
||||||
|
with open(_STATS, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({**_counts, "updated": int(now)}, f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _style_for(cats: dict) -> bytes:
|
||||||
|
sels = []
|
||||||
|
for cat, on in cats.items():
|
||||||
|
if on and cat in _COSMETIC:
|
||||||
|
sels.extend(_COSMETIC[cat])
|
||||||
|
if not sels:
|
||||||
|
return b""
|
||||||
|
sel = ",".join(sels)
|
||||||
|
# #584 — NO placeholder : collapse the ghosted ad slot entirely so the
|
||||||
|
# space disappears (display:none), rather than leaving a void/black-hole.
|
||||||
|
# Host-blocking (204) still saves the bandwidth ; this just hides the box.
|
||||||
|
rule = sel + "{display:none!important;visibility:hidden!important;}"
|
||||||
|
return (b"<style id=\"sbx-ghost-style\">" + rule.encode("utf-8") + b"</style>")
|
||||||
|
|
||||||
|
|
||||||
|
class AdGhost:
|
||||||
|
def requestheaders(self, flow: http.HTTPFlow) -> None:
|
||||||
|
f = get_filters()
|
||||||
|
if not (f.get("ad_ghost") and f.get("ad_ghost_block")):
|
||||||
|
return
|
||||||
|
if not _is_r3plus(flow):
|
||||||
|
return
|
||||||
|
host = flow.request.pretty_host or ""
|
||||||
|
if _AD_HOST.search(host):
|
||||||
|
flow.response = http.Response.make(
|
||||||
|
204, b"", {"X-SecuBox-Ghost": "blocked"})
|
||||||
|
_counts["blocked_requests"] += 1
|
||||||
|
_counts["bytes_saved_est"] += _EST_BYTES_PER_REQ
|
||||||
|
_flush()
|
||||||
|
|
||||||
|
def response(self, flow: http.HTTPFlow) -> None:
|
||||||
|
f = get_filters()
|
||||||
|
if not f.get("ad_ghost") or not _is_r3plus(flow):
|
||||||
|
return
|
||||||
|
if not flow.response or flow.response.status_code != 200:
|
||||||
|
return
|
||||||
|
ct = (flow.response.headers.get("content-type", "") or "").lower()
|
||||||
|
if "text/html" not in ct:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
body = flow.response.content or b""
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if not body or _MARK in body:
|
||||||
|
return
|
||||||
|
style = _style_for(f.get("ad_ghost_categories", {}))
|
||||||
|
if not style:
|
||||||
|
return
|
||||||
|
if _RE_HEAD.search(body):
|
||||||
|
new = _RE_HEAD.sub(style + b"</head>", body, count=1)
|
||||||
|
else:
|
||||||
|
new = style + body
|
||||||
|
flow.response.content = new
|
||||||
|
_counts["pages_cleaned"] += 1
|
||||||
|
_flush()
|
||||||
|
|
||||||
|
|
||||||
|
addons = [AdGhost()]
|
||||||
|
|
@ -479,9 +479,28 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
||||||
if utiq_n > 0:
|
if utiq_n > 0:
|
||||||
# 📡 N — operator-grade tracker active
|
# 📡 N — operator-grade tracker active
|
||||||
right_parts.append(f"📡 utiq:{utiq_n}")
|
right_parts.append(f"📡 utiq:{utiq_n}")
|
||||||
|
# #566 — ghost quick-stats: ads/widgets ghosted + bandwidth saved.
|
||||||
|
g_blocked = ctx.get("ghost_blocked", 0)
|
||||||
|
g_kb = ctx.get("ghost_kb", 0)
|
||||||
|
if g_blocked > 0:
|
||||||
|
# 🛡 N · ~X Ko économisés
|
||||||
|
right_parts.append(f"🛡 {g_blocked} ✕ ~{g_kb} Ko")
|
||||||
if ctx["asn"]:
|
if ctx["asn"]:
|
||||||
right_parts.append(_ncr(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 = 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
|
||||||
|
|
@ -644,6 +663,17 @@ class InjectBanner:
|
||||||
# AND R3 (portable WG opt-in). R0/R1 stay banner-free.
|
# AND R3 (portable WG opt-in). R0/R1 stay banner-free.
|
||||||
if _client_level(flow) not in ("r2", "r3"):
|
if _client_level(flow) not in ("r2", "r3"):
|
||||||
return
|
return
|
||||||
|
# #566 — modular filter toggle (toolbox WebUI). Banner can be
|
||||||
|
# disabled without touching the addon list.
|
||||||
|
try:
|
||||||
|
import sys as _sys
|
||||||
|
if "/usr/lib/secubox/toolbox" not in _sys.path:
|
||||||
|
_sys.path.insert(0, "/usr/lib/secubox/toolbox")
|
||||||
|
from secubox_toolbox.filters import get_filters as _gf
|
||||||
|
if not _gf().get("banner", True):
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# Phase 10 perf : cheap pre-flight check on Content-Length to avoid
|
# Phase 10 perf : cheap pre-flight check on Content-Length to avoid
|
||||||
# reading multi-MB bodies into RAM just to discover we'd skip them.
|
# reading multi-MB bodies into RAM just to discover we'd skip them.
|
||||||
# `flow.response.content` would buffer the whole body before returning.
|
# `flow.response.content` would buffer the whole body before returning.
|
||||||
|
|
@ -667,10 +697,25 @@ class InjectBanner:
|
||||||
# for R2) + level label ("R2" vs "R3")
|
# for R2) + level label ("R2" vs "R3")
|
||||||
try:
|
try:
|
||||||
ctx = _compute_site_context(flow)
|
ctx = _compute_site_context(flow)
|
||||||
|
# #566 — ghost quick-stats (ads/widgets ghosted + KB saved).
|
||||||
|
try:
|
||||||
|
import json as _json
|
||||||
|
with open("/run/secubox/ghost.json", "r", encoding="utf-8") as _gf2:
|
||||||
|
_g = _json.load(_gf2)
|
||||||
|
ctx["ghost_blocked"] = int(_g.get("blocked_requests", 0))
|
||||||
|
ctx["ghost_kb"] = int(_g.get("bytes_saved_est", 0)) // 1024
|
||||||
|
except Exception:
|
||||||
|
ctx["ghost_blocked"] = 0
|
||||||
|
ctx["ghost_kb"] = 0
|
||||||
csp_strict = _detect_csp_strict(flow)
|
csp_strict = _detect_csp_strict(flow)
|
||||||
report_url = _report_url_for(flow)
|
report_url = _report_url_for(flow)
|
||||||
level_label = _level_label(flow)
|
level_label = _level_label(flow)
|
||||||
level = _client_level(flow)
|
level = _client_level(flow)
|
||||||
|
# #566 — R3+/R4 is an active-protection tier (spoof + ghost),
|
||||||
|
# not mere inspection: relabel the banner status accordingly.
|
||||||
|
if level in ("r3", "r4") and ctx.get("status") == "inspected":
|
||||||
|
ctx["status"] = "protected"
|
||||||
|
ctx["status_icon"] = "\U0001F6E1" # 🛡
|
||||||
snippet = _banner_html_dynamic(_CA_SHA1, ctx, csp_strict,
|
snippet = _banner_html_dynamic(_CA_SHA1, ctx, csp_strict,
|
||||||
report_url, level_label, level)
|
report_url, level_label, level)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
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()]
|
||||||
|
|
@ -22,6 +22,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -61,7 +62,15 @@ _last_flush = 0.0
|
||||||
|
|
||||||
|
|
||||||
def _level() -> str:
|
def _level() -> str:
|
||||||
v = (os.environ.get("SECUBOX_PROTECTIVE_MODE") or "off").strip().lower()
|
# Modular filter config (toolbox WebUI) is the source of truth (#566);
|
||||||
|
# fall back to the SECUBOX_PROTECTIVE_MODE env when the config is absent.
|
||||||
|
try:
|
||||||
|
if "/usr/lib/secubox/toolbox" not in sys.path:
|
||||||
|
sys.path.insert(0, "/usr/lib/secubox/toolbox")
|
||||||
|
from secubox_toolbox.filters import get_filters
|
||||||
|
v = (get_filters().get("protective") or "off").strip().lower()
|
||||||
|
except Exception:
|
||||||
|
v = (os.environ.get("SECUBOX_PROTECTIVE_MODE") or "off").strip().lower()
|
||||||
return v if v in ("off", "alert", "spoof") else "off"
|
return v if v in ("off", "alert", "spoof") else "off"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ DEST_DIR="/var/lib/secubox/toolbox/webext"
|
||||||
DEST="${DEST_DIR}/secubox-toolbox-webext.xpi"
|
DEST="${DEST_DIR}/secubox-toolbox-webext.xpi"
|
||||||
# Tag-pinned (not /latest/): the webext release is make_latest:false so it
|
# Tag-pinned (not /latest/): the webext release is make_latest:false so it
|
||||||
# doesn't steal "latest" from the Android APK release. Bump on new webext-v*.
|
# doesn't steal "latest" from the Android APK release. Bump on new webext-v*.
|
||||||
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.2/secubox-toolbox-webext.xpi"
|
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi"
|
||||||
|
|
||||||
log() { logger -t "$MODULE" -- "$*" 2>/dev/null || echo "[$MODULE] $*" >&2; }
|
log() { logger -t "$MODULE" -- "$*" 2>/dev/null || echo "[$MODULE] $*" >&2; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,10 @@ fi
|
||||||
# protective_mode (#560) runs right after utiq_defense — early, so spoof-level
|
# protective_mode (#560) runs right after utiq_defense — early, so spoof-level
|
||||||
# header/cookie stripping happens before the logging addons record the flow.
|
# header/cookie stripping happens before the logging addons record the flow.
|
||||||
# Inert unless SECUBOX_PROTECTIVE_MODE=alert|spoof (default off).
|
# Inert unless SECUBOX_PROTECTIVE_MODE=alert|spoof (default off).
|
||||||
for addon in inject_xff utiq_defense protective_mode local_store social_graph inject_banner dpi cookies avatar ja4 soc_relay cert_pin_detect; do
|
# 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 media_stats; do
|
||||||
ARGS+=(-s "$ADDON_DIR/${addon}.py")
|
ARGS+=(-s "$ADDON_DIR/${addon}.py")
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1395,7 +1395,7 @@ async def wg_toolbox_apk() -> Response:
|
||||||
_WEBEXT_XPI = Path("/var/lib/secubox/toolbox/webext/secubox-toolbox-webext.xpi")
|
_WEBEXT_XPI = Path("/var/lib/secubox/toolbox/webext/secubox-toolbox-webext.xpi")
|
||||||
_WEBEXT_XPI_RELEASE = (
|
_WEBEXT_XPI_RELEASE = (
|
||||||
"https://github.com/CyberMind-FR/secubox-deb/releases/download/"
|
"https://github.com/CyberMind-FR/secubox-deb/releases/download/"
|
||||||
"webext-v0.1.2/secubox-toolbox-webext.xpi"
|
"webext-v0.1.4/secubox-toolbox-webext.xpi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2422,9 +2422,176 @@ async def admin_protective() -> dict:
|
||||||
out.update(_json.loads(st.read_text()))
|
out.update(_json.loads(st.read_text()))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
from .filters import get_filters as _gf
|
||||||
|
out["mode"] = _gf().get("protective", out["mode"])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/ghost")
|
||||||
|
async def admin_ghost() -> dict:
|
||||||
|
"""#566 — ad/banner ghoster savings (R3+/R4). Read-only counters."""
|
||||||
|
import json as _json
|
||||||
|
from pathlib import Path as _P
|
||||||
|
out: dict = {"blocked_requests": 0, "bytes_saved_est": 0,
|
||||||
|
"pages_cleaned": 0, "since": None, "updated": None}
|
||||||
|
try:
|
||||||
|
st = _P("/run/secubox/ghost.json")
|
||||||
|
if st.exists():
|
||||||
|
out.update(_json.loads(st.read_text()))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
out["mb_saved_est"] = round(out.get("bytes_saved_est", 0) / 1048576, 1)
|
||||||
|
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)."""
|
||||||
|
from .filters import get_filters
|
||||||
|
return get_filters(force=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/admin/filters")
|
||||||
|
async def admin_filters_set(request: Request) -> dict:
|
||||||
|
"""#566 — toggle mitm filters from the toolbox WebUI (write)."""
|
||||||
|
from .filters import set_filters
|
||||||
|
try:
|
||||||
|
patch = await request.json()
|
||||||
|
except Exception:
|
||||||
|
patch = {}
|
||||||
|
if not isinstance(patch, dict):
|
||||||
|
raise HTTPException(status_code=400, detail="json object expected")
|
||||||
|
return set_filters(patch)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/filters/ui", response_class=HTMLResponse)
|
||||||
|
async def admin_filters_ui() -> HTMLResponse:
|
||||||
|
"""#566 — minimal filter toggle panel for the toolbox WebUI."""
|
||||||
|
html = """<!doctype html><html lang=fr><meta charset=utf-8>
|
||||||
|
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||||
|
<title>Filtres ToolBoX</title>
|
||||||
|
<style>
|
||||||
|
body{background:#0a0a0f;color:#e8e6d9;font:14px system-ui,sans-serif;max-width:560px;margin:30px auto;padding:0 18px}
|
||||||
|
h1{color:#c9a84c;font-size:18px} h2{color:#6e40c9;font-size:13px;margin:18px 0 6px}
|
||||||
|
label{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #1a1a22}
|
||||||
|
select,input{accent-color:#6e40c9} .muted{color:#6b6b7a;font-size:12px}
|
||||||
|
.stat{display:inline-block;background:#12121a;border:1px solid #222;border-radius:6px;padding:6px 10px;margin:4px 4px 0 0}
|
||||||
|
#msg{color:#00ff41;min-height:18px;margin-top:10px}
|
||||||
|
</style>
|
||||||
|
<h1>🛡 Filtres ToolBoX</h1>
|
||||||
|
<p class=muted>Activation/désactivation à chaud des filtres mitm (R2/R3+). Prend effet en quelques secondes, sans redémarrage.</p>
|
||||||
|
<div id=stats></div>
|
||||||
|
<h2>Bannière & inspection</h2>
|
||||||
|
<label><input type=checkbox data-k=banner> Bannière de transparence (R2/R3)</label>
|
||||||
|
<label>Mode protecteur (spoof traceurs)
|
||||||
|
<select data-k=protective><option value=off>off</option><option value=alert>alert</option><option value=spoof>spoof</option></select></label>
|
||||||
|
<h2>Ghosting pub (R3+/R4)</h2>
|
||||||
|
<label><input type=checkbox data-k=ad_ghost> Masquer pubs/bannières/widgets (cosmétique)</label>
|
||||||
|
<label><input type=checkbox data-k=ad_ghost_block> Bloquer les hôtes pub/traceurs (économise la bande passante)</label>
|
||||||
|
<label><input type=checkbox data-c=ads> · catégorie : publicités</label>
|
||||||
|
<label><input type=checkbox data-c=consent_nag> · catégorie : bandeaux cookies/consentement</label>
|
||||||
|
<label><input type=checkbox data-c=newsletter> · catégorie : pop-ups newsletter</label>
|
||||||
|
<label><input type=checkbox data-c=social_widgets> · catégorie : widgets sociaux</label>
|
||||||
|
<p id=msg></p>
|
||||||
|
<script>
|
||||||
|
const $=s=>document.querySelector(s);
|
||||||
|
function post(p){fetch('/admin/filters',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(p)}).then(r=>r.json()).then(load).then(()=>{$('#msg').textContent='Enregistré ✓';setTimeout(()=>$('#msg').textContent='',1200)});}
|
||||||
|
function load(f){
|
||||||
|
document.querySelectorAll('[data-k]').forEach(el=>{const k=el.dataset.k;if(el.type==='checkbox')el.checked=!!f[k];else el.value=f[k];});
|
||||||
|
const c=f.ad_ghost_categories||{};document.querySelectorAll('[data-c]').forEach(el=>el.checked=!!c[el.dataset.c]);
|
||||||
|
}
|
||||||
|
function wire(){
|
||||||
|
document.querySelectorAll('[data-k]').forEach(el=>el.addEventListener('change',()=>{const v=el.type==='checkbox'?el.checked:el.value;post({[el.dataset.k]:v});}));
|
||||||
|
document.querySelectorAll('[data-c]').forEach(el=>el.addEventListener('change',()=>post({ad_ghost_categories:{[el.dataset.c]:el.checked}})));
|
||||||
|
}
|
||||||
|
function stats(){fetch('/admin/ghost').then(r=>r.json()).then(g=>{$('#stats').innerHTML=`<span class=stat>🛡 ${g.blocked_requests||0} bloqués</span><span class=stat>💾 ~${g.mb_saved_est||0} Mo économisés</span><span class=stat>🧹 ${g.pages_cleaned||0} pages nettoyées</span>`;});}
|
||||||
|
fetch('/admin/filters').then(r=>r.json()).then(f=>{load(f);wire();stats();});
|
||||||
|
</script></html>"""
|
||||||
|
return HTMLResponse(content=html)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/social/report/{token}.pdf")
|
@router.get("/social/report/{token}.pdf")
|
||||||
async def social_report_pdf(token: str) -> Response:
|
async def social_report_pdf(token: str) -> Response:
|
||||||
"""Phase 11.C (#508) — bilingual FR/EN evidence PDF for a peer.
|
"""Phase 11.C (#508) — bilingual FR/EN evidence PDF for a peer.
|
||||||
|
|
|
||||||
86
packages/secubox-toolbox/secubox_toolbox/filters.py
Normal file
86
packages/secubox-toolbox/secubox_toolbox/filters.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
#
|
||||||
|
# SecuBox-Deb :: toolbox :: modular filter config (#566)
|
||||||
|
#
|
||||||
|
# Single source of truth for which mitm filters are active, toggled from
|
||||||
|
# the toolbox WebUI (GET/POST /admin/filters). The mitm addons
|
||||||
|
# (inject_banner, protective_mode, ad_ghost) read this at flow time with a
|
||||||
|
# short cache so toggles take effect within seconds — no restart.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
FILTERS_PATH = "/etc/secubox/toolbox/filters.json"
|
||||||
|
|
||||||
|
DEFAULTS: Dict = {
|
||||||
|
"banner": True, # inject the R2/R3 transparency banner
|
||||||
|
"protective": "spoof", # off | alert | spoof (tracker spoofer)
|
||||||
|
"ad_ghost": True, # R3+/R4 silent ad/banner/widget ghosting
|
||||||
|
"ad_ghost_block": True, # 204 known ad/tracker hosts (save bandwidth)
|
||||||
|
"ad_ghost_categories": { # cosmetic ghost groups
|
||||||
|
"ads": True,
|
||||||
|
"consent_nag": True,
|
||||||
|
"newsletter": True,
|
||||||
|
"social_widgets": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_VALID_PROTECTIVE = ("off", "alert", "spoof")
|
||||||
|
|
||||||
|
_cache: Dict = {}
|
||||||
|
_cache_ts: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def get_filters(force: bool = False) -> Dict:
|
||||||
|
"""Merged filter config (defaults + on-disk overrides), 5 s cached."""
|
||||||
|
global _cache, _cache_ts
|
||||||
|
now = time.time()
|
||||||
|
if not force and _cache and (now - _cache_ts) < 5:
|
||||||
|
return _cache
|
||||||
|
out = json.loads(json.dumps(DEFAULTS)) # deep copy
|
||||||
|
try:
|
||||||
|
with open(FILTERS_PATH, "r", encoding="utf-8") as f:
|
||||||
|
disk = json.load(f)
|
||||||
|
if isinstance(disk, dict):
|
||||||
|
for k, v in disk.items():
|
||||||
|
if k == "ad_ghost_categories" and isinstance(v, dict):
|
||||||
|
out["ad_ghost_categories"].update(v)
|
||||||
|
elif k in out:
|
||||||
|
out[k] = v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if out.get("protective") not in _VALID_PROTECTIVE:
|
||||||
|
out["protective"] = DEFAULTS["protective"]
|
||||||
|
_cache = out
|
||||||
|
_cache_ts = now
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def set_filters(patch: Dict) -> Dict:
|
||||||
|
"""Merge a partial patch into the on-disk config and return the result.
|
||||||
|
Only known keys are accepted (defensive)."""
|
||||||
|
global _cache_ts
|
||||||
|
cur = get_filters(force=True)
|
||||||
|
for k, v in (patch or {}).items():
|
||||||
|
if k == "ad_ghost_categories" and isinstance(v, dict):
|
||||||
|
cur["ad_ghost_categories"].update(
|
||||||
|
{ck: bool(cv) for ck, cv in v.items()
|
||||||
|
if ck in DEFAULTS["ad_ghost_categories"]})
|
||||||
|
elif k == "protective" and v in _VALID_PROTECTIVE:
|
||||||
|
cur["protective"] = v
|
||||||
|
elif k in ("banner", "ad_ghost", "ad_ghost_block"):
|
||||||
|
cur[k] = bool(v)
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(FILTERS_PATH), exist_ok=True)
|
||||||
|
tmp = FILTERS_PATH + ".tmp"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(cur, f, indent=1)
|
||||||
|
os.replace(tmp, FILTERS_PATH)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_cache_ts = 0.0 # invalidate
|
||||||
|
return cur
|
||||||
|
|
@ -243,8 +243,9 @@ async function loadClients() {
|
||||||
const rows = (d && d.clients) ? d.clients : (Array.isArray(d) ? d : null);
|
const rows = (d && d.clients) ? d.clients : (Array.isArray(d) ? d : null);
|
||||||
if (!rows) { el.innerHTML = `<div class="empty">${(d && d.__error) || 'no data'}</div>`; return; }
|
if (!rows) { el.innerHTML = `<div class="empty">${(d && d.__error) || 'no data'}</div>`; return; }
|
||||||
if (!rows.length) { el.innerHTML = '<div class="empty">no clients</div>'; return; }
|
if (!rows.length) { el.innerHTML = '<div class="empty">no clients</div>'; return; }
|
||||||
|
const shown = rows.slice(0, 5); // #575 — cap the list to top 5
|
||||||
let html = '<table><thead><tr><th>MAC (hash)</th><th>IP</th><th>state</th><th>niveau</th><th>score</th><th>last</th><th>Actions</th></tr></thead><tbody>';
|
let html = '<table><thead><tr><th>MAC (hash)</th><th>IP</th><th>state</th><th>niveau</th><th>score</th><th>last</th><th>Actions</th></tr></thead><tbody>';
|
||||||
for (const c of rows) {
|
for (const c of shown) {
|
||||||
const ago = c.last_seen ? Math.round((Date.now()/1000 - c.last_seen) / 60) + 'm' : '—';
|
const ago = c.last_seen ? Math.round((Date.now()/1000 - c.last_seen) / 60) + 'm' : '—';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td><code>${c.mac_hash}</code></td>
|
<td><code>${c.mac_hash}</code></td>
|
||||||
|
|
@ -265,6 +266,9 @@ async function loadClients() {
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
|
if (rows.length > shown.length) {
|
||||||
|
html += `<div class="empty">+ ${rows.length - shown.length} autres clients (top 5 affichés)</div>`;
|
||||||
|
}
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,11 @@
|
||||||
// the densest tracker clusters become the visible "hot spots".
|
// the densest tracker clusters become the visible "hot spots".
|
||||||
const EYE_ID = 'eye:device';
|
const EYE_ID = 'eye:device';
|
||||||
|
|
||||||
|
// #575 — IPs are noise in this view (and produce a 2nd bubble next to
|
||||||
|
// the domain for the same metric). Hide every IP-only tracker node ;
|
||||||
|
// keep domains, labelled by country flag + name (never the IP).
|
||||||
|
const isIp = (s) => /^\d{1,3}(\.\d{1,3}){3}$/.test(s) || (s || '').includes(':');
|
||||||
|
|
||||||
// Build d3 dataset: sites are union of all node.sites + tracker nodes themselves.
|
// Build d3 dataset: sites are union of all node.sites + tracker nodes themselves.
|
||||||
const siteSet = new Set();
|
const siteSet = new Set();
|
||||||
for (const n of graph.nodes) for (const s of (n.sites || [])) siteSet.add(s);
|
for (const n of graph.nodes) for (const s of (n.sites || [])) siteSet.add(s);
|
||||||
|
|
@ -169,10 +174,11 @@
|
||||||
nodes.push({ id: 'site:' + s, label: s, kind: 'site' });
|
nodes.push({ id: 'site:' + s, label: s, kind: 'site' });
|
||||||
}
|
}
|
||||||
for (const n of graph.nodes) {
|
for (const n of graph.nodes) {
|
||||||
|
if (isIp(n.domain)) continue; // #575 — hide IP-only nodes
|
||||||
idx.set('tracker:' + n.domain, nodes.length);
|
idx.set('tracker:' + n.domain, nodes.length);
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: 'tracker:' + n.domain,
|
id: 'tracker:' + n.domain,
|
||||||
label: n.domain,
|
label: (n.country_flag ? n.country_flag + ' ' : '') + n.domain,
|
||||||
kind: 'tracker',
|
kind: 'tracker',
|
||||||
hits: n.hits,
|
hits: n.hits,
|
||||||
sites: n.sites,
|
sites: n.sites,
|
||||||
|
|
@ -194,6 +200,7 @@
|
||||||
links.push({ source: EYE_ID, target: 'site:' + s, reuse: 1, spoke: true });
|
links.push({ source: EYE_ID, target: 'site:' + s, reuse: 1, spoke: true });
|
||||||
}
|
}
|
||||||
for (const n of graph.nodes) {
|
for (const n of graph.nodes) {
|
||||||
|
if (isIp(n.domain)) continue; // #575 — no links to hidden IP nodes
|
||||||
const trackerKey = 'tracker:' + n.domain;
|
const trackerKey = 'tracker:' + n.domain;
|
||||||
for (const s of (n.sites || [])) {
|
for (const s of (n.sites || [])) {
|
||||||
links.push({
|
links.push({
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ R3 tunnel — no third-party calls.
|
||||||
Published release `.xpi` (downloadable directly):
|
Published release `.xpi` (downloadable directly):
|
||||||
|
|
||||||
```
|
```
|
||||||
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.2/secubox-toolbox-webext.xpi
|
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi
|
||||||
```
|
```
|
||||||
|
|
||||||
The toolbox also serves it from the cabine:
|
The toolbox also serves it from the cabine:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user