Compare commits

..

No commits in common. "master" and "v2.13.18" have entirely different histories.

2127 changed files with 9253 additions and 780661 deletions

View File

@ -3,922 +3,6 @@
--- ---
## 2026-07-01 — Macro subsystem (M2) + tor-exit reference kind (#771)
Services can now propose a vetted, AppArmor-confined **access macro** so an approved peer
consumes them. First increment: framework + `tor-exit` (SOCKS-over-mesh). Three packages:
- **secubox-annuaire 0.3.3** — optional signed `ServiceOffer.macro {kind, params}` that
federates in the signed payload (byte-stability guard keeps macro-less offers compatible
with pre-0.3.0 signatures); `annuairectl offer --macro-kind/--macro-param`.
- **secubox-macro 0.1.0 (NEW)**`secubox-macroctl` root dispatcher (kind allowlist, plugin
root-owned+non-writable tamper guard, src-ip mesh-CIDR check, euid-gated env-ignore,
append-only audit) + `macros.d/tor-exit` (nft grant/revoke, service_id path-traversal
sanitize, socks_port bound) + tight sudoers (env_reset) + AppArmor enforce (net_admin) +
postinst (Tor SocksPort on mesh IP, nft base set, audit pre-create).
- **secubox-p2p 1.9.0** — provider grant endpoint (self-signed Subscription auth: self-cert
DID + ed25519 over annuaire's canonical bytes; auto-mode only), consumer activate pulls
the credential over the mesh + runs macroctl activate, mesh listener :8798 (mesh-IP-only,
X-Real-IP), revoke-access, NNP=no for the sudo path, UI SOCKS endpoint + Revoke.
Built via SDD (8 tasks, per-task + final opus review = READY TO MERGE). The review loop caught
and fixed **6 Criticals**: offer-signature wire-break (macro:null changed signed bytes),
tor-exit root path-traversal (service_id as root filename), prerm mawk-bricks-dpkg, macroctl
NNP-blocks-sudo, macroctl env-injection root-RCE + missing root-ownership guard. Deployed the
3 debs to gk2 + c3box; **proven live on c3box under AppArmor enforce**: macroctl→tor-exit grant
adds a mesh IP to the `secubox_macro_torexit` nft set + returns the endpoint + writes an
append-only audit line; revoke empties it; bad-kind/non-mesh-IP/missing-src-ip all rejected.
Full cross-node federation + real Tor routing is env-blocked (Tor not installed on c3box; gk2
uses `inet filter` not `secubox_filter`) — not a code issue. Suites: annuaire 189, p2p 49,
macro 14.
---
## 2026-06-30 — secubox-p2p 1.8.0 : Service Registry = live view of annuaire catalog (#769)
Le registre de services p2p (`/p2p/`) affichait « No services registered » (JSON local
isolé), déconnecté du catalogue fédéré secubox-annuaire 0.2.0. Désormais c'est une **vue
live** : `GET /services` fusionne le catalogue annuaire + mes abonnements + un mince
*activation overlay* + les services p2p-locaux hérités (sans duplication, sans dérive).
`api/registry.py` (fusion pure, testable) + `api/annuaire_client.py` (lit
`/run/secubox/annuaire.sock`, jamais l'aggregator ; s'abonne EN TANT QUE nœud via la clé
0600 ; frappe un JWT de service car le subscribe annuaire est JWT-gated). 4 endpoints :
`GET /services` (dégrade en `catalog_unavailable`, ne 500 jamais), `POST
/services/auto-register` (active les offres locales + s'abonne aux distantes selon
auto/pending), `/{id}/request`, `/{id}/activate`. UI : bouton « Auto register all » +
Request access / Activate + badges d'état (service_id en encodeURIComponent — XSS-safe).
annuaire inchangé ; exécution des macros déférée au Milestone 2.
Construit via SDD (5 tâches TDD + revues par tâche + revue finale opus = READY TO MERGE).
Bug trouvé au déploiement : le subscribe annuaire est JWT-gated ET vérifie que le sujet est
un user activé → token de service frappé pour `admin` (override SBX_SERVICE_USER). 34 tests.
Déployé gk2 + c3box : `GET /services` = WAF mirror (local) + Suricata (fédéré de c3box) sur
gk2, image miroir sur c3box ; `auto-register` gk2 = {activated:1, requested:1} → Suricata
fédéré **approved**.
---
## 2026-06-30 — secubox-annuaire 0.2.0 : trustless cross-node service federation (#766)
Le `/services/pull` de 0.1.3 n'était **pas** réellement sans-confiance ni opérable :
`ingest_offer` vérifiait la signature contre une pubkey *fournie par l'appelant* sans
jamais contrôler `did_from_pubkey(pubkey) == provider` (forge « apporte ta clé, réclame
n'importe quel DID ») ; et `GET /services` renvoyait des offres **sans signature ni
pubkey** (le payload stocké les omet), donc un pair inconnu ne pouvait rien vérifier.
Corrigé :
- **Ingest sans-confiance** : `ingest_offer` impose `did_from_pubkey(pubkey)==provider`
AVANT la vérif de signature. did:plc = sha256(pubkey)[:32] → liaison auto-certifiante,
aucun annuaire, aucune confiance préalable.
- **Offres auto-portées** : `_get_offers`/`GET /services` ré-attachent `sig` + `signer_did`
+ `provider_pubkey` (métadonnée de transport, retirée avant reconstruction du modèle
extra=forbid). `pull_services` les consomme.
- **Bootstrap de nœud** : verbe `genesis()` (un nœud s'auto-atteste MEMBER fondateur,
brise le paradoxe invite/join ; DID auto-certifiant, `invited_by` vide → n'inflige
jamais la pluralité d'émancipation ; idempotent). `Op.GENESIS` (additif). CLI
`/usr/sbin/annuairectl` (init|whoami|status|offer|services|pull) opérant le journal
directement en tant que `secubox` (pas de JWT pour le bootstrap privilégié) ; clé 0600
dans `/etc/secubox/secrets/annuaire/node.key`.
- **Écouteur mesh** : `annuaire-mesh.conf.tpl` rendu par postinst sur l'IP wg-mesh du
nœud uniquement (`10.10.0.1:8799` sur gk2), `allow 10.10.0.0/24 + deny all`, GET
`/services` seul.
- **Tests** : +7 (forge, payload altéré, hex invalide, round-trip), 134 passants.
- **Revue sécurité** : aucune forge exploitable. Deux durcissements board-wide :
postinst valide l'écouteur rendu via `nginx -t` et le retire si échec (un rendu cassé
ne persiste jamais) ; livraison de `ip_nonlocal_bind=1` (nginx lie l'IP mesh même si
wg-quick@wg-mesh démarre après nginx).
Déployé sur gk2 (0.1.3 → 0.2.0) : service actif, `nginx -t` OK, écouteur live, genesis
gk2 (DID `0463…`) + offre « WAF mirror ». **Démo live** : un second nœud (fondateur
distinct, gk2 inconnu) `annuairectl pull http://10.10.0.1:8799` → ingested 1, chain_ok.
Reste : pull live gk2→c3box bloqué (clé SSH non autorisée sur .94) ; NIZK/PSI GK·HAM à venir.
---
## 2026-06-30 — secubox-yacy 1.0.12 : repair webui embed + navbar integration
Page admin `https://admin.gk2.secubox.in/yacy/` cassée sur deux points, corrigés dans
[`www/yacy/index.html`](../packages/secubox-yacy/www/yacy/index.html) :
- **webui (iframe récursif)** : l'`<iframe src="/yacy/">` pointait sur **cette même page**
(nginx sert `/yacy/` en `alias` statique, pas en proxy) → le panneau s'affichait
récursivement au lieu de l'UI YaCy. Le `src` est désormais construit au runtime depuis
`/api/v1/yacy/access`, en préférant l'URL **publique https** (`yacy.gk2.secubox.in`,
vérifiée sans X-Frame-Options/CSP → framable) pour éviter le blocage mixed-content ;
repli sur un lien « ouvrir dans un nouvel onglet » si seule une URL http LAN est joignable.
- **navbar** : la page utilisait une grille CSS `.layout` custom + `sidebar-light.css`
legacy, en conflit avec `sidebar.js` v2.33 (injecteur hybrid-skin). Migration vers le
pattern canonique (annuaire/cookies) : `design-tokens.css` + `crt-light.css`,
`<nav class="sidebar">` + `sidebar.js`, contenu dans `<main class="main">`. Strings
issues de l'API échappées avant injection.
Déployé live sur gk2 (`/usr/share/secubox/www/yacy/index.html`, backup `.bak-pre-fix`),
copie debian stagée synchronisée, bump 1.0.11 → 1.0.12. Assets `/shared/*` 200, JS
`node --check` OK.
**Cause racine réelle des cartes « unavailable » + « no search results »** : drift nginx
live. `/etc/nginx/secubox-routes.d/yacy.conf` (inclus par le vhost admin) avait dérivé vers
`proxy_pass …/aggregator.sock` en gardant le `rewrite` qui dénude le préfixe → l'aggregator
(modules montés sur le chemin **complet** `/api/v1/yacy/*`) recevait `/access` nu → 404 →
`.catch()` du JS → iframe jamais construit → pas d'UI YaCy. Réaligné sur la config livrée
(`yacy.sock`, ~0,2 s vs 11-20 s via l'aggregator qui bloquait sa boucle). YaCy jamais cassé
(freeworld, 352 global / 466 local pour « debian »). Même pattern que Lyrion #763 ci-dessous.
**Phase 2 — yacyctl detection + sudoers + postinst + .deb** :
- `yacyctl` reportait lxc « absent » / daemon « stopped » / overall **red** alors que tout
tournait : `secubox-yacy.service` tourne en `User=secubox` + `NoNewPrivileges=true`, et
`lxc-info`/`lxc-attach` exigent root (NNP bloque sudo). Remplacé par une **sonde réseau
privilège-free** (`curl http://$LXC_IP:$HTTP_PORT/`) — préserve le durcissement, signal
plus vrai. `lxc-info` gardé en enrichissement best-effort root-only. → vert via l'API.
- Ship `/etc/sudoers.d/secubox-yacy` (lxc-*) pour `yacyctl reload` (restart daemon in-LXC).
- `postinst` : `systemctl restart` inconditionnel — `deb-systemd-invoke restart` de
dh_installsystemd **refuse** de démarrer une unité *disabled* → upgrade laissait
`yacy.sock` absent → 502. (Piège de test : dpkg s'arrête sur un **prompt conffile** quand
on a édité les `/etc` à la main → `--force-confnew` pour aligner.)
- Construit + installé `secubox-yacy_1.0.12-1~bookworm1_all.deb` (output/debs/). Upgrade
propre validé : service active+enabled, dashboard vert, recherche 466, route `yacy.sock`.
---
## 2026-06-30 — Lyrion admin 404 → dedicated-socket extraction (#763)
Page `https://admin.gk2.secubox.in/lyrion/` : tous les widgets en **HTTP 404**.
### Cause racine
La route nginx `/api/v1/lyrion/` avait dérivé sur la board vers
`rewrite … /$1 break;` + `proxy_pass …/aggregator.sock;` (sans suffixe `/api/v1/lyrion/`).
L'aggregator monte les modules au chemin **complet** `/api/v1/lyrion/…`, donc le `/status`
dénudé → 404. (`curl aggregator.sock /api/v1/lyrion/status` → 200 ; `/status` → 404.)
### Décision — extraction socket dédié (comme auth/metrics)
Les handlers `now-playing` / `players` font du JSON-RPC LMS **bloquant** à chaque requête.
Sur la boucle unique de l'aggregator (~110 modules) un appel LMS lent fige toute la
passerelle → 502 board-wide (SPOF observé). lyrion repasse sur son propre
`secubox-lyrion.service` + `/run/secubox/lyrion.sock` + route nginx dédiée.
### Changements
- **source** `packages/secubox-lyrion/nginx/lyrion.conf` : invariant documenté (dedicated
socket, ne jamais folder dans l'aggregator).
- **source** `packages/secubox-lyrion/debian/postinst` : préserve l'état runtime sur upgrade
(`try-restart`), démarre au fresh install si le LXC LMS répond.
- **source** `packages/secubox-aggregator/sbin/secubox-aggregator-migrate` : `AGG_EXCLUDE`
(lyrion) → discovery + switch route + disable service le sautent (durabilité).
- **board** : `secubox-lyrion.service` enable --now ; route nginx (secubox.d +
secubox-routes.d) → `lyrion.sock` ; reload. 5 endpoints à 200, stream live affiché.
- **gotcha** : le 1er `enable --now` a re-chown `/run/secubox` (1777 root:root →
755 secubox:secubox) car le drop-in `no-runtime-dir.conf` (`RuntimeDirectory=`) n'était pas
rechargé en mémoire systemd. `daemon-reload` + restaure le parent à 1777 root:root → restart
ne le re-casse plus (boot-safe).
---
## 2026-06-27 — LAN standardisé 192.168.10.0/24 + c3box/gk2 live Freebox + bump 1.10.0 (#760)
Session terrain "c3box derrière Freebox" : la LAN SecuBox par défaut (`br-lan 192.168.1.1/24`)
entrait en collision avec la LAN d'un routeur opérateur courant (Freebox/Livebox en
`192.168.1.0/24`). En aval d'une Freebox, le WAN DHCP et la LAN se retrouvaient sur le **même
sous-réseau** → route dupliquée, ARP ambigu, IP de management injoignable.
### A. Constat live + remédiation immédiate
- **c3box** (second MOCHAbin) derrière Freebox : WAN `eth2=192.168.1.94` (bail Freebox) +
`br-lan=192.168.1.1/24``.94` injoignable depuis le LAN. Corrigé live : `br-lan → 192.168.10.1/24`.
SSH root activé, webadmin `https://192.168.1.94/` OK, `/dev/sda1` (931 G) monté sur `/data`
(style gk2 : UUID + nofail), partition eMMC retirée (`emmc-data`).
- **gk2** (live PoC) : uplink déplacé de `lan0` (DSA) vers le port cuivre WAN `eth2` ; netplan
réparé via **série** (gk2 hors-réseau le temps du switch) → `eth2 dhcp4: true`, `lan0` dépouillé.
Bail Freebox réservé sur le MAC eth2 `f0:ad:4e:27:88:9b` → gk2 reprend `192.168.1.200`. Persisté.
### B. Standardisation source (LAN = 192.168.10.0/24, gw .10.1) — 17 fichiers
- Netplans board : mochabin, espressobin-v7, espressobin-ultra, x64-vm, x64-live (`br-lan`),
+ unification VM vm-x64/vm-arm64 (`192.168.100.1 → 192.168.10.1`).
- Générateurs de netplan : `secubox-netmodes`, `secubox-hub` (preview), `secubox-net-detect`.
- dnsmasq (`espressobin-v7.conf`) : `dhcp-range` + `option:router` + `option:dns-server`.
- Scripts live-usb (mochabin/ebin) + SAN des certs auto-signés (`firstboot`, `build-image`,
`build-rpi-usb`, `build-live-usb`) → `IP:192.168.10.1`.
- **Hors scope (intacts)** : `192.168.255.1` (whitelist mgmt/trusted-proxy WAF/mail/wg/mitm),
listes `GATEWAYS` de sonde WAN, exemples remote-ui/round + tests.
### C. Release
- Bump mineur (« medium ») **1.9.0 → 1.10.0** : `build-image.sh`, `build-live-usb.sh`,
`build-ebin-live-usb.sh`, `build-rpi-usb.sh` (mochabin-live reste sur sa piste 2.0.0).
- Artefacts amd64 (x64) reconstruits depuis cette base.
---
## 2026-06-27 — Netboot live PROUVÉ + première install SecuBox Debian sur c3box (second MOCHAbin) (#748 #737)
Grande session hardware : netboot gk2→c3box validé de bout en bout, premier SecuBox Debian installé
sur un vrai MOCHAbin, et le blocage U-Boot qui empêche #748 de fermer est formellement documenté.
### A. Netboot gk2 → c3box : validé en prod
- **c3box** (second MOCHAbin, Armada 7040) a booté l'installeur SecuBox Debian servi par gk2 via
TFTP : factory U-Boot 2020.10 → `tftpboot Image/dtb/initrd``booti` → rescue shell installeur,
kernel custom 6.12.85 #5secubox. Le FIT signé (49 Mo) était servi en HTTP sur `:8099`.
- Le long détour cabling était une impasse LAB (prouvé via gk2 bridge-FDB + test DHCP) — aucun
bug logiciel.
- **Learnings opérationnels réutilisables** (documentés dans `wiki/Netboot-Install.md`) :
- Factory U-Boot 2020.10 s'interrompt sur **Enter** (pas Ctrl-C), `bootdelay=2`.
- Son env n'est PAS dans SPI mtd2 (env étranger fossile) → `fw_setenv` depuis Linux n'a aucun
effet ; seule la config U-Boot interne compte.
- Seul le port cuivre RJ45 unique = `mvpp2-2` est bootable par le factory U-Boot (les 4 ports
switch nécessitent le driver MV88E6XXX DSA, absent au boot).
- Kernel load à `0x02080000` = adresse mémoire réservée → crash immédiat ; utiliser `0x0a000000`.
- `setenv tftpblocksize 1468` pour TFTP rapide.
### B. #748 enhanced Tow-Boot (HTTP/wget bootloader) — DIFFÉRÉ, bloquant documenté
Branche `feature/748-enhanced-tow-boot-http-netboot-serial-fl` (stackée sur #737) :
spec+plan (`docs/superpowers/`), Kconfig Tow-Boot, `build-uboot-overlay.sh --tow-boot`,
plan serial-flasher, CI `.github/workflows/build-tow-boot.yml` (push-triggered).
**Bloquant dur (ciseau)** : le board MOCHAbin n'existe que dans le fork U-Boot 2022.07 de
Tow-Boot (pas de `wget`) ; `wget` n'existe que dans U-Boot stock ≥2023.07 (pas de board
mochabin/DTS). Bump à stock 2023.07 = `wget` compile mais build sans DTS. Pour débloquer :
backporter wget/TCP dans le fork Tow-Boot 2022.07, OU porter le board mochabin vers mainline
≥2023.07. Pas un tweak de config.
### C. PREMIÈRE INSTALL — c3box → SecuBox Debian (la headline)
- **Image** : artefact CI `secubox-mochabin-bookworm` (run 27426515472, 1,8 Go gzip / 8,0 Gio
décompressé), téléchargée sur gk2 `/data`, SHA256SUMS vérifié.
- **Signature** : clé `secubox-netboot.key` de gk2. Vérifié : cette clé FIT == `netboot-image.pub`
embarquée dans l'installeur (modulus match + roundtrip sign/verify). `sbx.img.gz` + `.sig`
publiés dans le root HTTP netboot, servis sur `:8099` (symlink depuis `/data`).
- **Install automatisé depuis le rescue shell** :
`wget sbx.img.gz` (en RAM, c3box a 8 Go) →
`openssl dgst -verify` contre `netboot-image.pub` (résultat : Verified OK) →
`gunzip | dd of=/dev/mmcblk0 bs=4M conv=fsync` (8 Gio, progression 32→62→94→100%) → sync.
- **c3box démarre SecuBox Debian v1.9.0** — hostname `secubox-mochabin`, kernel Debian
6.1.0-47-arm64, stack complète : secuboxd, hub, grafana, zigbee, mqtt, authelia,
sentinel/rogue-BTS (layers WALL+MIND). Creds root/secubox, Web UI `:9443`.
- **Fix auto-boot persistant** : l'image utilise `extlinux.conf` à `0x02080000` (adresse réservée
factory U-Boot → reset immédiat) et ne livre pas de `boot.scr` compilé. Construit
`/boot/boot.scr` (kernel@`0x0a000000`, initrd@`0x10000000`, `console=ttyS0` + earlycon,
`root=LABEL=rootfs`) : le factory U-Boot charge `boot.scr` depuis mmc et démarre Debian sans
intervention. **VÉRIFIÉ** : reboot sans intervention → login Debian.
- **Layout eMMC installé** : GPT p1=boot (FAT, `/boot`) p2=ROOT (`/`) p3=DATA. c3box était
OpenWrt ; eMMC écrasé (install RAM-only, pas de risque sur l'OS tournant avant le `dd`).
- **Rig netboot temporaire gk2 encore actif** : `lan1=192.168.77.1/24`, dnsmasq test (DHCP) sur
`lan1`, `nft iif lan1 accept`, nginx boot-vhost extra listen `192.168.77.1:8099`.
## 2026-06-24 (cont.) — R4 analyst mode: MITM-everything + media reverse-catcher + clone (#736)
New "R4" doctrine — visibility over performance. Delivered + live on gk2:
- **Splice flip**`tls-splice-seed.conf` reduced from a media-CDN perf list to
breakers-only (`api.anthropic.com`); splice now applied ONLY where MITM provably
breaks (cert pinning). Banner reaches every page; catcher sees media URLs. Live:
learned splices cleared, autolearn gated (`tls_splice=off`).
- **sbxmitm media reverse-catcher** (`cmd/sbxmitm/mediacatch.go`, toolbox-ng 0.1.20)
— 2xx MITM'd flows → cloneable media URLs (HLS/DASH manifests, direct A/V,
googlevideo videoplayback) appended to `/run/secubox/media-catch.jsonl` (URLs
only, deduped, atomic, fail-open). `--media-catch` default on; worker unit
`ReadWritePaths=/run/secubox`.
- **mediaflow Discovered Media + Clone** (2.1.0) — `/discovered`, `/clone`
(yt-dlp→ffmpeg queue, lazy worker for the aggregator), `/library`,
`/download/{id}`, DELETE; dashboard cards. Verified: HLS caught → ffmpeg →
464 MiB mp4 in library. yt-dlp installed.
- Also fixed the empty mediaflow dashboard (2.0.2 contract + 2.0.3 cumulative
services): cards/streams live, Top Media Services from DPI cumulative store.
KEY: dashboard routes via the **aggregator** (in-process import) — restart
`secubox-aggregator` to pick up mediaflow code changes.
- Phase 4 done — R4 button added to the banner topbar (R0..R4) + set-level + by-MAC
validation + analytics buckets; gated to the wg path like R3 (secubox-toolbox 2.7.20).
- yt-dlp upgraded 2023.03.04 → 2026.06.09 (standalone binary; YouTube works).
- Recos: catcher now captures YouTube watch **pages** (kind=page, toolbox-ng 0.1.22);
Discovered Media persisted off tmpfs into a durable capped store (mediaflow 2.1.1);
yt-dlp packaged (Recommends + weekly refresh timer + postinst).
- **Catch-log ownership bug**`/run/secubox/media-catch.jsonl` was created
`secubox`-owned while the worker runs as `secubox-toolbox`, so O_APPEND failed
silently → nothing captured. Fixed with a tmpfiles.d entry pre-creating it owned
by the writer every boot (zz-secubox-toolbox-ng.conf). Live: rm + worker recreate.
## 2026-06-24 (cont.) — Banner on nonce-CSP sites + Claude API splice + YouTube unblock (#728)
Three distinct root causes behind "no banner on youtube / news", fixed in order:
1. **Trusted Types** (0.1.17) — `require-trusted-types-for` blocked DOM injection. Stripped.
2. **Nonce-based CSP** (0.1.18) — the banner is *inlined* (service-worker-proof), but a CSP
nonce/hash makes `'unsafe-inline'` IGNORED → the bare inline `<script>` was silently
blocked. `relaxCSPForLoader` now **borrows the page's own nonce** and stamps it on the
injected `<script nonce=…>` (surgical: page CSP/nonces/hashes untouched), falling back to
forcing `unsafe-inline` (drop nonce/hash/strict-dynamic) only when there's no nonce.
Nonce validated to base64 charset (attribute-breakout guard). Threaded nonce through
injectIntoBody → injectHTML → injectInlineBanner. Tests rewritten for inline semantics.
3. **YouTube wholly blocked** (runtime) — autolearn false-positive put `youtube.com` in
`/var/lib/secubox/toolbox/learned-trackers.txt``Decide()` returned `block` (204) →
page never loaded. Removed from learned + added to `ad-allowlist.txt` (hot-reloaded).
Latent-bug tracker: **#735** (autolearn must not block apex/first-party nav targets).
**Claude API splice** (user request) — `api.anthropic.com` added to `tls-splice-seed.conf`
(+ live seed): cert-pinned Claude API/SDK clients reject the MITM CA, so pass them through;
`claude.ai` web stays MITM'd (browser trusts the CA → still gets the banner).
Verified end-to-end on gk2: YouTube 200 + banner nonce == page nonce; lemonde/lefigaro
banner via unsafe-inline fallback. DPI confirmed healthy — collector writes to
`/var/lib/secubox/dpi/` (state.json/cumulative.json fresh), `/exfil` returns categorized
flows; the earlier "empty" was me checking the wrong paths (`/run/secubox/dpi`).
## 2026-06-24 — DPI YouTube bannering: strip Trusted Types CSP (#728)
- **Root cause** — YouTube serves a standalone `Content-Security-Policy:
require-trusted-types-for 'script'` header. sbxmitm's `relaxCSPForLoader` already
relaxed `script-src` (drop `strict-dynamic`, add `'self'`/`'unsafe-inline'`) so the
banner loader runs, but Trusted Types still blocked the banner's DOM injection →
banner silently never mounted on YouTube.
- **Fix** (`cmd/sbxmitm/csp.go`, toolbox-ng 0.1.17) — drop `require-trusted-types-for`
and `trusted-types` directives during the relax; omit the resulting empty CSP header
line. Local Go unit tests cover both the relax and the empty-header drop.
- **DPI capture half** — collector `state.json` was stale (frozen 09:44); restarted
`secubox-dpi-flowcap` → fresh windows, YouTube/media flows now visible in mediaflow.
- Deployed to gk2; R3 workers `secubox-toolbox-ng-worker@1..4` restarted on 0.1.17.
- Filed for later: #729 wireguard peers/tabs, #730 yacy, #731 lyrion, #732 magicmirror,
#733 firewall dashboard misreport, #734 webui.conf hardcoded-route cleanup.
## 2026-06-22 — DPI exfil engine + Netrunner report (HTML+PDF) + sbxmitm fixes
Big session: full per-device DPI exfiltration pipeline, the kbin report reborn as a
cyberpunk-netrunner character sheet, and two live-ops fixes on the Go MITM engine.
All PRs merged to master and deployed live on gk2.
### DPI — per-device cloud-exfiltration (#687, secubox-dpi 1.0.5 → 1.1.2)
- **Phase 1** nDPI flow-DPI on `wg-toolbox` (ndpiReader, ~1% CPU on the Armada).
- **Phase 2** Go collector (`secubox-dpi-collector`, pure stdlib, arm64): attributes
flows to devices via `sha256(wg_pubkey)[:16]`, classifies SNI into nDPI-style
**categories** (cloud/filehost/messaging/ai/media/game/social/adult), fires exfil
scenarios (`exfil_volume`, `new_cloud`, `beaconing`, `unclassified_external`).
Producer = `secubox-dpi-flowcap` (60s windows) → `GET /api/v1/dpi/exfil`.
- **Dashboard** (#693/#695): "Cloud Exfiltration Watch" panel + stat cards + all list
cards repointed off the inactive netifyd to the live exfil engine.
- **#692** beaconing tuned to a C2-plausible cadence (1s1h, CV≤0.25, external).
- **#705 cumulative 7d** — `cumulative.json` so the report shows history, not just the
last 60s window (was: idle device → all zeros).
- **Packaged** `secubox-dpi 1.1.x` (arch arm64, Go built in debian/rules offline,
flowcap auto-enabled, `Depends: libndpi-bin`).
### kbin report — Cyberpunk-Netrunner character sheet (#707, HTML + PDF)
- **#699** report tabs (Pistage / DPI-Exfil / Overall) with donut charts.
- **#701/#703** DPI stats + visual donut charts in the PDF (mitm/certs/ads/dpi).
- **#707** persona sheet: class+emoji from the request UA (live device), level=R3 for
wg peers, ICE/Exposition bars, XP, 4 pip-bar CARACTÉRISTIQUES, Inventaire, Bestiaire,
Quêtes — HTML neon + PDF `_persona_block`.
- **#709** carto hub map + emoji tables (Traceurs/Pays/DPI) in the PDF.
- **#711/#712** "En un coup d'œil" added to the PDF.
- **#714** charts switched to **matplotlib PNG** embeds (fpdf2 vector donuts were blank
in iOS/Chrome viewers).
- **#716** donut grid → ONE combined 2×2 image (was spilling each donut/legend onto its
own page → 24 pages). Report back to a clean 4 pages. User: "report parfait".
### sbxmitm (Go MITM engine, #662 line)
- **#689** forged leaf cert TTL **24h → 365d** — root cause of recurring "certificat
expiré" on clients (cache never evicts; 24h leaves expired daily). Interception kept.
- **#697** stop truncating responses >8MiB — `streamResponse()` streams non-injected
bodies verbatim; large **Gmail** messages/attachments rendered again over R3.
- **#688** own-domain splice approach REJECTED (decision: intercept all vhosts) — reverted.
### Ops notes
- Surf-break incident: R3 mitm CA rotated 2026-06-05 → clients must re-import the CA root
(the "expired cert" was client-side trust, not the board).
- R3 engine is the Go `sbxmitm` (`secubox-toolbox-ng-worker@1..4`, 10.99.1.1:8091-8094)
— NOT the Python mitm; restart THOSE for R3 changes.
---
## 2026-06-20 — kbin Tor shipped + client releases + ad-block/mitm hardening
- **#683 MERGED (PR #684)** — kbin Tor egress quick-switch (switch + nft owner-match
tunnel, own-services exemption, reconciler+timer), dashboard/landing/banner metrics
fixes, 🧅 indicators (banner/webext/APK), APK persistent WG identity, landing+report
**redesign** (verdict gauge + donut/bars + collapsible details). Live on gk2; Tor armed.
- **Client releases served from kbin**: `android-v0.4.0` (Latest) + `webext-v0.1.5`
published by CI; pinned webext tag bumped; board fetch-helpers pull them →
/wg/toolbox.apk (0.4.0) + /wg/toolbox.xpi (0.1.5). toolbox 2.7.12.
- **#685 ad-learner hardened (2.7.13)** — NEVER_LEARN guard (Google/CDN/fonts/captcha/
auth/payment), AD_MIN_SITES 1→2, prune existing. Root cause of euronews breakage:
the learner had 204'd `www.google.com` → broke reCAPTCHA/consent. Also allowlisted
www.google.com/.fr live.
- **mitm-wg stream_large_bodies=1m (2.7.14)** — large binary downloads (APK, CA) were
corrupted ONLY through the R3 tunnel (HTTP/2 buffer/reframe); now passed verbatim.
- **OPEN [#686]** — android-toolbox non-root flow broken (CA auto-install needs root,
WG handoff → Play Store, tunnel not detected). Needs on-device dev/testing; rooted-vs-
non-rooted decision pending. #685 signing was a red herring (corrupt = mitm buffering).
## 2026-06-19 — kbin Tor egress quick-switch implemented DARK (#683, ToolBoX 2.7.1)
- **Switch + tunnel** for routing kbin surfing through Tor, shipped **default-OFF /
fail-closed** on `feature/683`. Reuses existing secubox components per the user ask.
- **Transport decision (USER): torify the MITM egress.** nft owner-match on the
`secubox-toolbox` (mitm-wg) uid → Tor TransPort 9040 / DNSPort 5353. Clients →
TPROXY → mitm decrypts/ad-blocks/poisons/banners/re-encrypts → exits via Tor.
**Inspection fully preserved**; only the exit IP + network identity change. (Rejected:
SOCKS5 Go-core dialer = blocked on #662; transparent client torify = breaks inspection.)
- **Switch**: `filters.json` flags `tor_mode`/`tor_preset`; API (kbin-gated, admin.gk2
only for actions) `GET/POST /admin/tor/{state,on,off,newnym,check-leaks}`; 🧅 WebUI tab
(badge bootstrap/circuits/exit-IP, toggle, NEWNYM, SOCKS leak probe). `tor_ctl.py`
reuses secubox-tor's control-port code — no cross-service JWT.
- **Tunnel arms via reconciler**: root, path-triggered (`secubox-toolbox-tor.path`
watches filters.json) → portal stays `NoNewPrivileges=true`, no sudo. nft loaded
BEFORE tor (no clearnet window); IPv6 worker egress dropped (no v6 leak); prerm
disarms on real removal (not upgrade). Depends jq; Recommends tor + python3-socksio;
postinst adds secubox-toolbox to debian-tor group.
- **Verified**: 166 toolbox tests green (10 new), nft syntax valid (user-resolve only),
maintainer scripts `sh -n` clean, license headers OK, changelog parses 2.7.1.
- **Granularity = global kbin Tor mode** (owner-match can't be per-client). Per-client
(WG-hash) Tor tracked under #662 (Go-core SOCKS5 dialer). NOT yet flipped/deployed —
needs soak + off-board leak test + tls_splice(#649)-OFF before arming.
---
## 2026-06-19 — kbin milestone: ToolBoX 2.7.0 (middle release) + Tor chapter staged (#683)
- **End-of-session checkpoint** — docs + positioning + version, no runtime behaviour change.
- **`secubox-toolbox` 2.6.59 → 2.7.0** (middle release) — caps the 2.6.x line
(ad-intelligence / Anti-Track v2 / anti-bot uTLS #662) and opens the **kbin** chapter:
kbin (`kbin.gk2.secubox.in`, the public ToolBoX portal) framed as the *first tool of the
CyberMind Swiss-army cyber kit* — transparent performance, full-encrypted MITM inspection,
ad poison/smog injection, adware-ban transparency banner, safe browsing.
- **Docs** — new wiki use-case `docs/wiki/Kbin-Toolbox.md`, `docs/FAQ-KBIN-TOR.md`,
README positioning blurb.
- **Plan #683 (issue + spec)** — kbin **Tor endpoint**: a quick-switch re-routing consenting
client surfing through Tor (outbound egress, pseudo-network) so the kbin exit is anonymized.
Spec `docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md`. Invariants:
inspection preserved (Tor after the forging core), fail-closed, opt-in/default-OFF, no DNS
leak, CSPN audit-logged. Opposite direction of `secubox-exposure` (inbound hidden services);
reuses its Tor control. Depends on the #662 Go core for the preferred SOCKS5-dialer transport.
- **Caveat recorded** — Tor mode must force `tls_splice` (#649) OFF per-client or asset flows
leak the real IP.
---
## 2026-06-19 — #662 anti-bot: Chrome TLS fingerprint (uTLS) — defeat DataDome without splice (PR #674)
- lemonde.fr (DataDome) blocked R3 navigation at the 2nd level: the engine re-origined
upstream TLS with a Go JA3/JA4 → flagged as bot. Splice rejected (don't exempt a
tracking site). Fix: upstream transport now presents a real **Chrome** fingerprint
via **uTLS HelloChrome_Auto + h2-over-uTLS**. Verified live: JA4
`t13d1516h2_8daaf6152771_02713d6af862` (Chrome), was Go.
- **Cert verification preserved** (manual verifyUConn: system roots + intermediates +
hostname; adversarially tested). Stopped the Accept-Encoding downgrade (was a tell) +
added brotli/zstd decode-inject-reencode. H1 response-header timeout.
- First vendored deps (utls/brotli/zstd/x-net, pure-Go), offline arm64 via -mod=vendor.
Canary 1 worker → verified Chrome FP + cert chains + ad-block + banner → widened to 4.
- Caveat: DataDome also fingerprints HTTP/2 + behaviour — uTLS helps strongly, not a
100% guarantee. Browser test is the real confirmation.
## 2026-06-19 — #662 post-cutover restore: ad-block metrics + popup CSS (PR #673)
- **Found by verification**: the cutover ported the 204-block but NOT ad_ghost's
metrics recording (frozen since 2026-06-18 18:59) nor its cosmetic/popup-hiding CSS
(popups returned — they're 1st-party DOM, never touched by host-204).
- **Metrics**: Go aggregates blocks in-memory (per ad_host/site + per mac_hash), flushes
every 10s to a new portal `POST /__toolbox/ad-event` (unauth R3-perimeter, body-bounded,
never 500s) → SQLite store → #ads dashboard live again (total_blocked rising).
- **Popups**: Go injects `<style id="sbx-ghost-style">` on R3 HTML (wg-gated, idempotent,
on the gzip path with the banner) — ports `_COSMETIC` + ad-specific popup tokens
(interstitial/ad-overlay/popup-ad/popunder/exit-intent), conservative (no bare
modal/popup/overlay, regression-tested). Verified live on the R3 path.
- toolbox-ng 0.1.5 deployed (rolling restart) + portal api.py hot-deployed (drift closed
at next .deb build). Portal uvicorn boot ~14s.
## 2026-06-18 — #662 Phase 7: Python R3 engine DECOMMISSIONED + nft persistence
- **nft persistence** (master `eea46326`): the boot re-apply source is the drop-in
`/etc/nftables.d/zz-secubox-toolbox-wg-fanout.nft` (loaded by nftables.service). Edited
it `808x→809x` (live already 809x → zero disruption), `nft -c -f` validated reboot-safe;
patched the repo source `packages/secubox-toolbox/nftables.d/secubox-toolbox-wg-fanout.nft`.
- **Python decommissioned**: `disable --now secubox-toolbox-mitm-wg-worker@{1..4}` +
`-mitm-wg-dynreload.path` → 8081-8084 free, **~240M RAM freed**. Units kept (disabled)
for emergency rollback. **Kept** `secubox-toolbox-mitm.service` (R2 captive-AP mitm on
10.99.0.1:8080 — a different path; the cutover was R3-only). Also pointed the board's
`/usr/share/.../secubox-toolbox-wg-fanout.nft` → 809x so a postinst re-run can't revert
to dead ports.
- **Verified self-sufficient with Python gone**: banner injects on gzip HTML, ads 204,
redirects relayed 301.
- Deliberately did NOT rebuild+reinstall the secubox-toolbox .deb (portal-restart blip +
board-wide nft reload, gratuitous) — repo source is 809x, the next natural build closes
the installed-payload drift. **#662 epic complete: Go engine sole R3 MITM, fast, ~64MB
vs ~280-470MB, persistent, ad-block + banner + redirects all correct.**
## 2026-06-18 — #662 R3 CUTOVER to the Go MITM engine (PR #670) — LIVE + banner ported
- **Cutover executed and live.** The Go engine now serves **100% of R3 traffic**,
replacing the Python mitmproxy workers. Found + fixed 4 blockers that made the dark
package unable to serve the live path: (1) it forged with the wrong CA (ca-wg "WG CA"
vs the "R3 CA" clients trust) → now uses the mitmproxy confdir bundle; (2) root-only
key vs non-root user → R3 CA bundle is group-readable; (3) bound 127.0.0.1 vs the
10.99.1.1 DNAT target → now binds 10.99.1.1; (4) ran CONNECT vs transparent → now
`--transparent`. `loadCA` scans PEM blocks by type (combined cert+key bundle).
- **Validated on real arm64 hardware** then rolled out gated: localhost forge against
the real R3 CA → scoped-DNAT transparent capture → **canary slot 3 (~25%, dead-man
armed)** → **widen to 100%**. At 100%: 0 restarts, 0 errors, ~64MB total
(vs Python ~280-470MB), even round-robin, 142 distinct SNIs/75s.
- **Banner ported** (the one regression the user caught — "no more banner but fast").
Go now injects the real loader `<script src="/__toolbox/loader.js" data-mh=.. data-wg=..>`
(guard-idempotent, R3 wg flag, mac_hash identity) and reverse-proxies
`/__toolbox/loader.js`+`/__toolbox/bundle` to the portal (127.0.0.1:8088, fail-open),
keeping bundle/level logic in Python. Verified live: loader injected + assets 200.
- **Rollback** = one `nft replace` (Python workers kept warm). **Persistence gap**: the
nft flip is a live edit, not yet in the drift-managed generator → reboot safely falls
back to Python (workers enabled, banner intact). Phase 7 (decommission Python +
persist nft) deferred to a soak'd follow-up.
## 2026-06-18 — #662 MITM engine migration: P5-prep + P6-prep (PRs #668, #669, all DARK)
- **P5-prep (PR #668).** Wired the ported `Decide`+jar into the Go engine's request/
response handlers: `handleConnect` runs allow/splice/block/mitm; `anonymizeRequest`
(strip operator/re-id headers + DNT/GPC) on every MITM'd flow; cookie-poison gated
to mitm+tracker only (never allow/own-infra; fail-closed-to-clean; benign cookies +
Set-Cookie attrs preserved). New `secubox-toolbox-ng` debian pkg builds an arm64
`.deb` shipping `/usr/sbin/sbxmitm` + a **DISABLED** `worker@.service` on `:809%i`
(no enable/start, no nft). 22 Go tests, reviewed APPROVED.
- **P6-prep (PR #669).** No-traffic build-out of the live transparent path, still DARK.
`machash.go` ports `mac_hash_of`/`_wg_hash_of` (WG peers → `sha256(pubkey)[:16]`,
mtime-cached, fail-open) wired into `clientHashFromConn`, cross-engine parity vs
Python (anti-rig verified). Transparent `SO_ORIGINAL_DST` accept (`--transparent`,
default off): peeks ClientHello SNI WITHOUT decrypting → Decide → **splice = true raw
passthrough** (never `tls.Server`) / else forge via replayable `prefixConn`; upstream
TLS verifies by SNI, pins captured ip:port. Two-stage review caught + fixed a
splice-decrypt defect. Builds linux/arm64+amd64+darwin, vet clean, race green, Python
parity 10 passed. CONNECT path + poison gate byte-unchanged.
- **Engine now functionally complete + packaged, entirely DARK.** Remaining work =
the production DEPLOYMENT phases (shadow → cutover → decommission), which touch live
R3 traffic and are deferred to a deliberate watched session — NOT chained off "go".
## 2026-06-18 — #656 Ad Intelligence (PR #657, toolbox 2.6.56) + splice reverted
- **Ad Intelligence — learn/act/measure.** `ad_ghost` now records every
block/silent per (ad_host, site=registrable(Referer), action) into a new
`ad_block_stats` store (in-memory dicts, bg-thread flush — no SQLite on the
proxy hot path), exposed via `GET /admin/ad-stats` + a new **#ads dashboard
tab** (top ad hosts, ads-blocked-per-site, action split, KB saved). Aggressive
learning: 3rd-party ad-shape requests captured as `ad_candidates`; autolearn
`_ad_feed` promotes hosts on ≥AD_MIN_SITES (default 1) distinct sites into the
204'd blocklist. Safety (inverts the splice mistake — learning to BLOCK is
reversible): `ad-allowlist.txt` always wins, `ad_learn` toggle, every block
visible in metrics, no IP-drop, no CSP weakening. 115 tests green; deployed +
verified (/admin/ad-stats 200, metrics flowing, ad_ghost intact).
- **Splice (#649/#651) REVERTED to off.** `tls_splice=on` bypassed the whole
addon chain → autolearn promoted telemetry/tracker hosts (datadog/MS/newsroom)
to splice → ad_ghost/anti-track bypassed → ads returned. Flipped `tls_splice=off`
(full MITM, ad-blocking restored). Splice perf vs ad-blocking is a fundamental
conflict; needs media-only-no-learn rework before any re-enable.
- **Banner #653 reverted** (async loader can't read currentScript → inline-bundle
was dead code; setupReassert regressed the banner). Board on 2.6.55-equivalent
banner. The strict-CSP/SPA banner gap (YouTube) is the browser-extension's job
(webext content-script WIP on `feature/655`, paused).
## 2026-06-18 — #649 selective SNI-splice (Lever A) shipped dark (PR #650, toolbox 2.6.54)
- **Architecture decision.** Asked "do we need a full mitm for R3 HTTPS?" Answer:
outbound HTTPS interception intrinsically needs per-host cert forging (the
WAF/own-cert analogy doesn't transfer) — so we keep a forging MITM but only
decrypt flows we'd actually modify. Plan = A-then-B: **A** = selective
SNI-splice (this), **B** = Go/Rust core (strategic, later). WAF deferred.
- **Lever A.** New `tls_splice` addon (first in the mitm-wg chain) decides at the
TLS ClientHello, from the SNI alone, whether to MITM or **splice** (raw
passthrough — no forge/decrypt/parse/16-addons). Policy: curated media-only seed
(googlevideo/ytimg/fbcdn/twimg/scdn…, deliberately NOT generic CDN edges)
autolearn-promoted never-HTML hosts (`splice_host_obs` table, ≥20 obs,
html_hits==0). Never splices trackers/fortknox/no-SNI/media_cache-on. Learning
obs recorded off the event loop (bg thread), only for undecided hosts.
- **Dark-launch.** Ships `tls_splice=observe` (classify + log would-splice, still
MITM — zero behavior change); `on` flip is post-soak; `off` kill-switch.
- **Built TDD** (7 tasks, 102 tests), two-stage reviews per task + whole-branch
review (APPROVED; closed a hot-path sync-SQLite issue → bg-thread offload, and a
fortknox-WebUI never-set refresh gap). **Deployed gk2 2.6.54**, rolling restart
of the 4 workers, addon loads clean, 0 runtime errors, dark default confirmed.
Next: soak → review → flip `on`.
## 2026-06-18 — #623 systemic shared-parent clobber resolved at source (PR #648)
- **Root cause corrected.** The recurring `/var/{lib,log,cache,…}/secubox` parent
clobber was NOT the `install -d -m 0750 /parent/leaf` leaf form (empirically
proven harmless: GNU `install -d -m` modes only the final component). It was the
scaffold boilerplate `install -d -m 750 /var/lib/secubox` + `/run/secubox` (BARE
parents) in ~56 module postinsts — written `-m 750` (3-digit), which is why prior
greps/sweeps (#511/#627/#631) missed it.
- **Source-wide fix.** Scripted rewrite of all bare-parent targets → `/run/secubox`
1777 root:root, `/var/lib|log|cache|etc|usr/share/secubox` 0755; 6 multi-arg
lines split per-parent (4 were setting `/var/lib/secubox` world-writable 1777 —
a security regression); 3 `chmod 750 /var/log/secubox` (soc-gateway/soc-agent/
ui-manager) → 0755. Module-private leaves (`/var/lib/secubox/<mod>` 0750) left
untouched. Scaffold `new-package.sh` + `.claude/PATTERNS.md` fixed so new
packages don't reintroduce it. secubox-core 1.1.8 tmpfiles.d now declares all 5
shared parents at 0755 (mode-only) for boot/install-time self-heal.
- **Verified:** all 64 changed maintainer scripts `bash -n` clean; zero bare-parent
restrictive lines remain (install-d + chmod forms); saas-relay + core rebuilt and
packaged postinst/tmpfiles confirmed. Two-stage review (found + closed 2 gaps:
the chmod-form clobbers + tmpfiles coverage). NOT mass-deployed (60-pkg restart =
thundering-herd risk); live covered by `secubox-dirs-guard.timer`; lands at next
CI image build / reflash.
## 2026-06-18 — perf sprint (hub latency, R3 tunnel encoding) + crowdsec unblock
- **Hub dashboard latency (#644, PR #645, hub `1.4.6`).** The hub runs mounted in
`secubox-aggregator` (no sub-app lifespan → cold caches); cold `/dashboard` fanned
out ~16 sequential `systemctl is-active` (9-12 s) and `/public/health-batch` did an
uncached 3.3 s `list-units`. Fix: `_ensure_services_warm()` (one batched offloaded
`is-active`, double-checked lock vs thundering herd) on dashboard/status/modules/
alerts; `_refresh_health_batch()` TTL snapshot served by the bg loop, cold-miss =
one offloaded call. **Verified live: health-batch 3.3 s → 8 ms** (77 modules, shape
unchanged). Toolbox `/admin/clients/rich` enrichment capped to the 12 most-recent.
- **R3 tunnel web-load (#646, PR #647, toolbox `2.6.53`).** Diagnosed live: 4-core
board at load ~5; the 4 mitm-wg workers are GIL-bound (~1 core total, ceiling ~30%/
worker) competing with R2-mitm/gitea/metrics/crowdsec. Hot path already cached. The
one code fix: `inject_banner` forced `Accept-Encoding: identity` on EVERY document
for stream-inject, but streaming is disqualified on CSP-strict sites + when upstream
compresses → those pages pulled uncompressed (3-5× bytes) through the worker for
zero benefit. Now adaptive: keep gzip/br by default, learn per-host eligibility
(`_STREAM_VERDICT`, capped/self-healing), strip identity only on proven-eligible
hosts' next visit. No feature loss; workers came back leaner (72 MB vs 117 MB).
Deploy via detached `dpkg -i` + rolling sequential restart of the 4 workers.
- **crowdsec unblocked.** Its postinst's `cscli hub update` had 403'd
(cdn-hub.crowdsec.net) leaving it half-configured (blocking apt). Re-tested → the
403 was TRANSIENT CloudFront throttling (HTTP/2 200, real Amazon cert, not WAF-
intercepted); `dpkg --configure crowdsec` → RC=0, `dpkg --audit` clean. No patch.
## 2026-06-15 — gitea mis-route fix + robust WAF route propagation
- **gitea (`git.maegia.tv`) 404 → 200.** Pure routing-table error: its WAF
route pointed at `192.168.1.200:8000` (unrelated nginx) instead of the gitea
LXC `10.100.0.40:3000`. Corrected the route; gitea container was healthy
throughout. (`gitea.gk2`→nginx:9080 and `git.gk2`→gitea:3000 were already OK.)
- **Robust route propagation (#609/PR #610, mitmproxy 1.0.8 + waf 1.2.6).**
Fixing gitea surfaced that the #603 *file* bind-mount binds an inode, so route
tools (`jq > tmp && mv` = new inode) didn't reach the addon until a container
restart. Now: **directory** bind-mount (host `/srv/mitmproxy`
`/var/lib/secubox-waf-routes`, ro) + symlink, and the addon **live-reloads**
`haproxy-routes.json` on mtime change (10 s throttle, in `requestheaders`).
Verified live: `jq+mv` add → `[routes] live-reloaded 256 routes`, **0
restart**. Ported to source (both synced `secubox_waf.py` copies + wafctl) +
rebuilt into apt.secubox.in.
## 2026-06-15 — WAF hardening + perf: close open-proxy, behind-WAF media cache
Follow-up to the WAF restoration. Three findings investigated; two fixed.
- **Open forward-proxy / loops (#605/PR #606, mitmproxy 1.0.6 + waf 1.2.4).**
`--mode regular` + HAProxy `default_backend mitmproxy_inspector` made the WAF
an open proxy: internet scanners (114.66.25.146, 211.154.17.165,
hashtagbrock.nl) drove a **72% backend-error rate** + 11 self-loop 508s/hr.
The `requestheaders` hook now serves ONLY our vhosts (routes / our domains
via routes-derived `local_suffixes` → nginx :9080 / `SELF_HOSTS`) and returns
**421 with no upstream connect** otherwise. Live: 0 external server-connects,
0 loop-508s, apt/admin/kbin 200, scanners 421.
- **Behind-WAF media cache (#607/PR #608, mitmproxy 1.0.7 + waf 1.2.5).** New
`media_cache.py` addon caches cacheable GET media/static (image/video/audio/
font/css/js) from our vhosts on disk (URL key, 16 MB/obj, 2 GB LRU, TTL from
`max-age`) and serves repeats from cache — backend-load + latency win for
hosted media. **Not a bypass**: requests still pass `secubox_waf` inspection;
only the response body is served from a WAF-populated cache. Toggle
`/data/mitmproxy/media-cache.json` (default on). Live: `X-SecuBox-Cache: HIT`.
Gate fix vs the toolbox copy: cache on body length (our nginx is chunked).
- **WG R3 tunnel** (`wg-toolbox`, 4 peers, 4 `mitm-wg-worker@{1..4}`) is
healthy — not the bottleneck; the WAF open-proxy churn was. All fixes ported
to source (both synced `secubox_waf.py` copies) + rebuilt into apt.secubox.in.
**Still optional:** relax the forced `Connection: close` (FD-leak fix #496) to
bounded keep-alive now that scanner churn is gone — lower per-request latency.
## 2026-06-15 — APT repo: all packages published + signed (apt.secubox.in)
Made the apt repo at `https://admin.gk2.secubox.in/repo/` (served from
`/var/www/apt.secubox.in`, manager `repoctl`/reprepro) carry **all** packages.
- **Was broken**: pool had 15 orphan debs with an **empty reprepro DB** and no
working signature — the published signing key `packages@secubox.in`
(fp 31848880…) has **no private key on the board**.
- **Signing** (user chose on-board `apt@secubox.in`, fp 219BA872…): imported its
secret into the repo GPG home (`/var/lib/secubox-repo/gpg`), wrote
`conf/distributions` (`SignWith: 219BA872…`) + `conf/options`, re-published
`secubox-keyring.gpg` + `FINGERPRINT.txt`. `InRelease`/`Release.gpg` now
**Good signature**. (install.sh doesn't pin the fp — transparent.)
- **Built all 144 packages** (`-d`, arch:all) + `reprepro includedeb bookworm`
→ 288 entries (×2 arch), 145 debs in pool, current versions
(core 1.1.6, threat-analyst 1.4.4, vm 1.0.1, toolbox 2.6.37, hub 1.4.3).
WebUI `/api/v1/repo/packages` lists 288. Served + signed via nginx :9080.
- **Tooling fix**: `scripts/build-packages.sh` now passes `-d` to
dpkg-buildpackage (it omitted it → dpkg-checkbuilddeps silently dropped
secubox-core and others from every build). 1 pkg failed (sentinelle-gsm,
buildinfo artifact race — deb still produced).
**Public HTTPS now works — WAF mitmproxy restored (3 stacked bugs).** The WAF
LXC (`mitmproxy`, served via HAProxy `mitmproxy_inspector` → 10.100.0.60:8080)
was down board-wide (every inspected vhost 503/400), blocking public
`apt.secubox.in`. Three compounding faults, all fixed live on gk2:
1. **Crash-loop** (restart #45552): the `cookie-audit.conf` systemd drop-in
(added #156) overrode `ExecStart` but dropped `--set confdir=/data/mitmproxy`
→ mitmdump fell back to `~/.mitmproxy`, which `ProtectHome=true` blocks →
`PermissionError: config.yaml`. Restored the flag in the drop-in (+ copied
the existing CA into `/data/mitmproxy` to preserve identity).
2. **mitmproxy-11 routing**: the LXC addon (`secubox_waf.py`, pre-#499) only
redirected upstream in the `request` hook, but mitmproxy 11 opens the
upstream connection *before* `request` → traffic went to the public IP
(82.67.100.75). Added a `requestheaders` hook that sets
`flow.server_conn.address` (+ request host/port) before the connect.
3. **Route-file drift** (the real killer, `routes_count: 0`): the addon reads
`/data/mitmproxy/haproxy-routes.json`, but the system maintains
`/srv/mitmproxy/haproxy-routes.json` (255 routes). The addon's file was
missing. Fixed by **bind-mounting** the host file into the container at the
addon's path (`/var/lib/lxc/mitmproxy/config`) so they stay in sync.
Verified: `apt-get update` against `https://apt.secubox.in` fetches a
**GPG-signed** InRelease + Packages (no signature errors), apt sees 130
secubox packages, `.deb` downloads (200). Other inspected vhosts recovered.
Live fixes are durable (container rootfs + LXC config survive restarts);
porting them into the provisioning package is a follow-up.
## 2026-06-15 — threat-analyst: global security overview (1.4.3, live on gk2)
`secubox-threat-analyst` 1.4.1 → 1.4.3, merged via **PR #598 (closes #597)**,
built + deployed live on gk2.
- **#597** — threat-analyst page becomes a **global security overview**: all
metrics dynamic, fed live from WAF + CrowdSec + firewall. New cached
`/overview` endpoint (double-buffer, 60 s background refresh →
`overview.json`) aggregating WAF (`/run/secubox/waf.sock /stats`: threats
today, blocked 24 h, rules loaded), CrowdSec (detection: alerts), firewall
(enforcement: IPs blocked in nft via crowdsec-firewall-bouncer). WebUI gains
a "Vue globale sécurité" card row + source health line (`loadOverview()` in
`loadAll()`).
- **Privilege-safe sourcing**: daemon runs as unprivileged `secubox` user →
`cscli`/`nft list` (both root-only) failed silently. Switched to CrowdSec's
privilege-free **Prometheus :6060** (`cs_alerts` + `cs_active_decisions`).
No privilege escalation, no coupling to broken `secubox-blacklist-sync`.
- Also carried the **1.4.2 build-safe postinst** fix (#595/#596) which had
not yet reached the board (was at 1.4.1; `deb-systemd-helper` enable).
- Live verified: CrowdSec 3712 alerts / 29312 active decisions, firewall
29312 blocked, WAF 140 rules; `/overview` 200 via socket **and** aggregator
proxy (aggregator restarted to re-discover the new route).
**Found, not fixed (separate):** `secubox-blacklist-sync.service` is **failed**
(#521, exit 2) → `secubox_blacklist` nft sets empty. Does not affect the
overview (firewall count comes from the bouncer via Prometheus).
### 1.4.4 — real CrowdSec ingestion (#599, PR #600)
The overview cards populated, but the **headline stats + Top-N leaderboards
stayed 0**: `collect_crowdsec_alerts()` shelled out to bare `cscli`, which
fails for the unprivileged `secubox` user → `alerts.jsonl` empty.
- **Read-only sudo ingestion** (backend only; frontend stays value-only):
collector now runs `sudo -n /usr/bin/cscli alerts list -o json -l 200`.
Ships `/etc/sudoers.d/secubox-threat-analyst` (only `cscli alerts/decisions
list *`, read-only), `visudo`-validated in postinst (self-removes if bad).
- **`NoNewPrivileges=no`** on the unit so sudo can escalate — matches the
sibling `secubox-crowdsec` / `secubox-waf` units (`NoNewPrivileges=yes`
had blocked sudo: "no new privileges flag is set").
- **Auto-collect loop** (~5 min) fills the DB without the page open; severity
mapped correctly (`remediation` is a bool).
- **Dedup + 48 h compaction**: `get_recent_alerts` dedups by id, `compact_
alerts()` bounds the append-only log (was inflating counts/leaderboards).
- Live verified (1.4.4): `alerts_24h=12`, **13 unique IPs, 10 countries**
(BG/BR/DE/FR/ID/IE/JP/NL/SG/US), 6+ scenarios → stats + leaderboards real.
### secubox-vm 1.0.1 — /vm/ showed 0 containers (#601, PR #602)
`https://admin.gk2.secubox.in/vm/` reported 0 containers though gk2 runs 20
LXC (16 running). Two compounding bugs:
- **Privilege**: the **aggregator mounts each module in-process** as the
unprivileged `secubox` user (serving model confirmed:
`/usr/lib/python3/dist-packages/aggregator/main.py` imports
`/usr/lib/secubox/<name>/api/main.py`). Bare `lxc-ls` can't see root's
`/var/lib/lxc` → empty.
- **Wrong `-F` key**: `lxc-ls -F MEMORY` is rejected (`Invalid key`) and emits
no rows — valid key is `RAM`.
Fix (backend-only): LXC read+lifecycle via `sudo -n` (`run_priv`); ships
`/etc/sudoers.d/secubox-vm` (`lxc-ls/info/start/stop`, visudo-validated);
`lxc-create`/`destroy` stay root-only (endpoints carry no JWT); `lxc-ls -F
…,RAM`; postinst reloads `secubox-aggregator`. KVM/libvirt readings were
already correct (`/dev/kvm` absent, libvirtd off). Live: `containers
{total: 20, running: 16}`, `/vms` lists all 20.
## 2026-06-14 — ToolBoX privacy/perf sprint : 2.6.23 → 2.6.36, all live on gk2
Large feature sprint on `secubox-toolbox` (built + merged + deployed live,
kbin healthy) + clients + two live fixes. Each shipped via PR + merge +
build + deploy.
**Toolbox (`secubox-toolbox` 2.6.23 → 2.6.36):**
- #560 protective mode — tracker alerting + active **spoofer** (strip
operator/tracking headers, drop 3rd-party cookies, DNT/GPC). Live in
`spoof` on the 4 R3 workers + R2.
- #566 modular **filters** (`/etc/secubox/toolbox/filters.json`, WebUI
`/admin/filters/ui`) + R3+/R4 **ad/banner ghoster** (ad-hiding CSS +
204 ad/tracker hosts ; savings → banner quick-stats).
- #584 ad ghosting = **collapse** (no placeholder ; reverted #576 black-hole).
- #577 shared **media proxy-cache** (image/video-segment, 16 MB/obj cap,
2 GB LRU, default OFF/opt-in) — `/admin/cache`.
- #589/#591 **autolearn** bad trackers → ad_ghost block set (threat-intel
domains + operator-grade cross-site ; anti-bot excluded) + hourly timer.
- #553/#549 cartographie **donut** (continent→country) + #587
**domain-nugget** cloud (country→eTLD+1) + #575 **IP nodes hidden**
(flag+name only) + #555 **favicons** of major sites (never IPs).
- #545/#572 banner: neon → colourful **emoji-chip guirlande** ;
inspected→**protected** on R3+/R4 ; #578 shared **pin** broadcast
(`/admin/pin/ui`).
- #570 DPI **media/content-type statistifier** + donut (`/admin/media/ui`).
- #574 webext popup **protection panel** ; #568 top-tracker list capped 5.
- #562 `/ca/fingerprint` surfaces the **R3 CA** (D5:E4:3A) on the tunnel.
- #581 **postinst fix** : enabled units get a real `restart` on upgrade
(was leaving the portal dead → kbin 503 ; bit us twice).
- #516 review (#564): `detect_antibot` → (vendor, **is_challenge**),
response-level (cf-mitigated / non-200 token) — deployment vs challenge.
**Clients:** Android APK **v0.3.0** (real zero-tap : launch + boot
auto-onboard) ; webext **v0.1.4** (crash-fix const-ext, favicons, popup
protection panel) — both served from the cabine + GitHub releases.
**Live fixes:** Nextcloud iPhone photo sync (disabled broken
`files_antivirus` + raised PHP upload limits) ; kbin 503 root-caused →
#581.
**Open / blocked:** #592 unified webmail-hub (Gmail OAuth2 + Gandi + OVH) —
design filed, BLOCKED on a Google OAuth client + operator decisions.
## 2026-06-13 — Browser extension : emancipate cartographie live (ref #532)
Nouveau client `clients/webext-toolbox/` (MV3 Firefox `.xpi` + Chromium),
sœur de l'app Android. Surface la cartographie sociale R3 dans le
navigateur : badge live des traceurs + popup (4 tuiles + mini Round-Eye
graph SVG sans dépendance + top-traceurs taggés CDN/anti-bot/opérateur +
actions cartographie/PDF/RGPD-wipe). Parle uniquement à la cabine via R3
(pas de CORS backend grâce à host_permissions).
`secubox-toolbox 2.6.14` : `GET /wg/toolbox.xpi` (local sinon 302 →
release), bouton onboard, helper `secubox-toolbox-fetch-xpi`, postinst
dir. CI `build-webext.yml` (`web-ext lint` + build, release asset sur tag
`webext-v*`). Suivi : signature AMO, SSE `/social/live`, icône PNG
Chromium, Poke/Emancipate (#525).
---
## 2026-06-13 — Android ToolBox app : serve + root-mode silent onboarding (ref #531/#536/#538)
App compagnon Android one-tap R3 (`clients/android-toolbox/`, Kotlin + Compose).
- **#531** — scaffold Gradle/Compose + CI `build-android-apk.yml` (debug APK
artifact, release asset sur tag `android-v*`). CI green.
- **#536** — `GET /wg/toolbox.apk` (build local sinon 302 → release GitHub) +
bouton onboard kbin + helper `secubox-toolbox-fetch-apk`.
- **#538** (PR #539) — root-mode silent onboarding : install CA système
(bind-mount cacerts + APEX conscrypt, SELinux ctx, `subject_hash_old`
pur Kotlin) + WireGuard natif noyau + vérif R3 auto, gated derrière le tap
`⚡ Installation automatique (root)`. Fallback handoff app WireGuard.
Fichiers `RootShell.kt`, `RootOnboard.kt`, step `RootAuto`. CI APK build
green (code compile).
- Suivi : release signing (keystore CI) pour empreinte publiée stable.
---
## 2026-06-11 — Phase 12.C + Phase 13 protection enforcement plane COMPLETE (ref #518-#528)
`secubox-toolbox 2.6.6 → 2.6.11`, tags v2.13.16 → v2.13.19.
### Phase 12.C — operator-grade / state-adjacent (#518, v2.13.16, 2.6.7)
`detect_operator_grade` : telco header-enrichment (MSISDN/x-acr/WAP),
operator-consortium (Utiq/TrustPid), data-broker / state-adjacent hosts
(LiveRamp/BlueKai/Acxiom/Neustar/Tapad/Experian/Palantir-class). Top
severity void-purple lens + double ring + ⛔ banner + PDF evidence
section. Detection only.
### Phase 13 — protection enforcement plane (#519) COMPLETE
Made the SecuBox ban plane (Vortex DNS + WAF + CrowdSec) actually enforce
on device browsing across every egress path.
- **13.A** (#521, v2.13.17, 2.6.8) — `inet secubox_blacklist` nft table,
v4/v6 interval+timeout sets, single forward-hook drop chain (covers
captive/WG/br-lxc/LAN); `secubox-blacklist-sync` unions CrowdSec bans +
threat-intel C2 (2h timeout); /admin/blacklist. **Also fixed the
override_dh_strip latent bug** (never runs for arch:all → nft/unbound/
nginx/perf drop-ins had stopped shipping; root cause of live-config
drift) by moving to execute_after_dh_auto_install. Memory saved.
- **13.B** (#522, v2.13.17, 2.6.9) — DNS-guard: resolve blocklisted
domains → IPs into the set (closes DoH/hardcoded-IP bypass); count-only
DoH/DoT detection chain (15 v4 + 6 v6 providers); SECUBOX_DOH_BLOCK
opt-in. create-or-replace idiom → idempotent reloads.
- **13.C** (#524, v2.13.18, 2.6.10) — per-device attribution: rate-limited
SBX-BL-DROP/SBX-DOH nft logs → journald tailer → device_blocks
(anonymous WG/lease hash); quarantine set + /admin/quarantine + one-click
operator action.
- **13.D** (#527, v2.13.19, 2.6.11) — feedback loop: escalation evaluator
reads opgrade/antibot/device-blocks aggregates, escalates over threshold
to blacklist IPs / cscli decision / device quarantine. Audit-logged,
reversible, **all sources default OFF** (opt-in via SECUBOX_ESCALATE_*).
**Doctrine** : DEFAULT DROP preserved (policy accept only adds drops); no
WAF bypass; anonymous (rotating mac_hash); all escalations TTL'd +
reversible + opt-in. Verified live on gk2 (18 C2 IPs enforced, quarantine
add/remove, synthetic escalation + audit entry).
### Future idea captured (#525)
Phase 14 deception plane — pseudo-responses from a proxy instead of
dropping tracker IPs (indistinguable, pollutes the profile) + neutralizing
CDN-preloaded tracking scripts. For later.
---
## 2026-06-10 (soir) — Phase 11 COMPLETE + Phase 12.A/B + toolbox tabs — v2.13.15 (ref #502-#516) ## 2026-06-10 (soir) — Phase 11 COMPLETE + Phase 12.A/B + toolbox tabs — v2.13.15 (ref #502-#516)
Consolidated stack merged via PR #517. `secubox-toolbox 2.5.2 → 2.6.6`, Consolidated stack merged via PR #517. `secubox-toolbox 2.5.2 → 2.6.6`,
@ -6926,19 +6010,3 @@ CONFIG_USB_NET_RNDIS_HOST=y
- LAN interfaces scanned: lan0, lan1, lan2, lan3, br0, br-lan, eth0, eth1 - LAN interfaces scanned: lan0, lan1, lan2, lan3, br0, br-lan, eth0, eth1
- ARP states mapped to online: REACHABLE, DELAY, PROBE, PERMANENT = online - ARP states mapped to online: REACHABLE, DELAY, PROBE, PERMANENT = online
- STALE, FAILED = offline - STALE, FAILED = offline
## 2026-06-24 — build+deploy T0 fixes (#494/#519/#53/#421) + dirs-guard /run self-heal
- Merged #121/#53/#65; cherry-picked #494 onto master (versions re-bumped above
master's advanced core 1.1.8/hub 1.4.6 → core 1.1.9, hub 1.4.7).
- Discovered #494 was systemic (7 pkgs chowning /run/secubox parent) AND that
91 services declare `RuntimeDirectory=secubox` → systemd re-chowns the parent
to secubox:secubox 0755 on each start (#421). Central fix: extended
secubox-dirs-guard to re-assert /run/secubox 1777 root:root every minute
(core 1.1.10) instead of editing 91 units.
- Built + deployed to gk2 (8 pkgs): core 1.1.10, hub 1.4.7, eye-remote 1.0.1,
metablogizer 1.2.2, metrics 1.0.4, p2p 1.7.1, wazuh 1.0.1, toolbox 2.7.18.
First deploy ssh was timeout-killed mid-toolbox-postinst → recovered with
dpkg --configure -a (cleared stale lock). Verified: /run/secubox=1777 root:root
holds, 0 half-configured, all services + R3 workers active, webui/portal 200,
toolbox blacklist-sync (#519) carried.

View File

@ -383,13 +383,9 @@ case "$1" in
adduser --system --group --no-create-home --home /var/lib/secubox secubox adduser --system --group --no-create-home --home /var/lib/secubox secubox
fi fi
# Répertoires runtime — SHARED parents, NE JAMAIS les passer en 0750/0700 # Répertoires runtime
# (#623 : casse la traversée pour les daemons non-secubox → kbin/toolbox 500). install -d -o secubox -g secubox -m 750 /run/secubox
# /run/secubox reste 1777 (sticky world-writable, sockets de tous les services, install -d -o secubox -g secubox -m 750 /var/lib/secubox
# #471) ; /var/lib/secubox reste 0755. Les leaves privées
# (/var/lib/secubox/<module>) peuvent être 0750.
install -d -o root -g root -m 1777 /run/secubox
install -d -o secubox -g secubox -m 755 /var/lib/secubox
# Activer et démarrer le service # Activer et démarrer le service
systemctl daemon-reload systemctl daemon-reload

View File

@ -1,226 +1,10 @@
# TODO — SecuBox-DEB Backlog # TODO — SecuBox-DEB Backlog
*Mis à jour : 2026-06-27* *Mis à jour : 2026-06-10*
---
## ⚪ T5 — Images / OS variants / Hardware (ajouts 2026-06-27)
### ⬜ MOCHAbin — bootloader propre (adresses réservées + extlinux)
> Workaround actif : `/boot/boot.scr` compilé forçant le kernel à `0x0a000000`. Fix durable requis.
- [ ] **Option A — Corriger l'image** : patcher `extlinux.conf` généré par le CI pour utiliser
`0x0a000000` (kernel) et `0x10000000` (initrd) au lieu de `0x02080000` (adresse réservée
factory U-Boot 2020.10 → reset immédiat). Boot.scr deviendrait redondant.
- [ ] **Option B — Enhanced Tow-Boot (#748)** : bloqué par le ciseau U-Boot (voir ci-dessous) ;
déverrouille wget/HTTP natif dans U-Boot, supprime le besoin de TFTP pour les futures installs.
- [ ] **Valider** que le fix d'adresse tient sur les deux MOCHAbin (gk2 + c3box).
### ⬜ #748 — wget dans U-Boot pour MOCHAbin (bloquant documenté)
> Bloquant dur (ciseau) confirmé 2026-06-27. Branche
> `feature/748-enhanced-tow-boot-http-netboot-serial-fl` : spec + plan + Kconfig +
> `build-uboot-overlay.sh --tow-boot` + CI `.github/workflows/build-tow-boot.yml` en place.
> Problème : board mochabin UNIQUEMENT dans fork Tow-Boot U-Boot 2022.07 (pas de `wget`) ;
> `wget`/TCP UNIQUEMENT dans stock U-Boot ≥2023.07 (pas de board mochabin/DTS).
- [ ] **Voie 1** : backporter le stack TCP + `wget` de U-Boot ≥2023.07 dans le fork Tow-Boot
2022.07 (mochabin board natif). Diff TCP = `net/wget.c` + dépendances `CONFIG_NET_WGET`.
- [ ] **Voie 2** : porter le board mochabin (DTS Armada 7040 + PHY + eMMC) vers U-Boot mainline
≥2023.07 (sans Tow-Boot). Plus long mais durable.
- [ ] Choisir une voie, débloquer #748.
### ⬜ Packager le flow netboot + install signé (rig temporaire → procédure reproductible)
> Actuellement rig manuel sur gk2 : `lan1=192.168.77.1/24`, dnsmasq DHCP, nft, nginx `:8099`.
- [ ] Scripter la publication de l'image signée dans le root HTTP netboot (wget + sha256 + sig).
- [ ] Documenter / packager la config dnsmasq + nft + nginx pour un segment `lan1` dédié.
- [ ] Intégrer dans `scripts/deploy-netboot.sh` ou équivalent.
### ⬜ Teardown rig netboot temporaire gk2
> Le rig (lan1 bridge, dnsmasq, nft iif lan1 accept, nginx extra listen) reste actif jusqu'à
> ce que c3box soit autonome en prod.
- [ ] Retirer la règle nft `iif lan1 accept` (risque : tout le segment lan1 est accepté sans filtrage).
- [ ] Désactiver / retirer dnsmasq test sur lan1.
- [ ] Retirer le extra listen `192.168.77.1:8099` du vhost nginx netboot (ou couper le vhost si
plus nécessaire).
- [ ] Vérifier que c3box auto-boot sans rig (boot.scr en place → OK).
---
## ✅ Clos 2026-06-22 — DPI exfil + report Netrunner + sbxmitm
- ✅ **#687 DPI exfil pipeline** — flowcap + Go collector + dashboard + cumulatif 7j,
packagé `secubox-dpi 1.1.2` (inclut #692/#693/#695/#705).
- ✅ **#707 report kbin = fiche Netrunner** HTML+PDF (#699/#701/#703/#709/#711/#714/#716).
- ✅ **#689** sbxmitm cert 365d · **#697** stream >8MiB (Gmail) · **#688** splice rejeté.
### DPI Phase 3
- [x] Enrichissement **ASN** (GeoLite2-ASN) pour l'egress sans SNI — **#719 mergé, live**
(`secubox-dpi 1.1.3`, maxminddb-golang vendored).
- [x] **Historique + timeline par device****#721 mergé, live** (`secubox-dpi 1.1.4`,
buckets quotidiens `history.json` 14j + `/api/v1/dpi/history` + panneau Timeline
dashboard). NB : JSON daily buckets (pas SQLite — pas de driver CGO dans le binaire
statique ; SQL riche reportable si besoin).
- [x] Démon **nDPId****évalué puis ÉCARTÉ** (#722/#723 revertés). Raison perf :
ndpiReader tourne en fenêtres bornées (Nice 15, ~1% CPU, libère le cœur entre
les passes) ; nDPId = démon permanent + nDPIsrvd → CPU/RAM **continue** sur une
board déjà saturée (load ~4.6/4 cœurs). Gain (JSON riche, pas de respawn) <
risque. **Décision : on garde ndpiReader** comme producteur du pipeline exfil.
(Le build CI QEMU a aussi échoué au 1er essai → chemin fragile en plus.)
### ⬜ Cosmétique report PDF (non bloquant)
- [ ] Glyphes drapeaux régionaux → lettres (police embarquée). Option : drapeaux PNG.
- [ ] Chiffres espacés dans certaines cellules (fallback police).
### ⬜ APK on-device #685/#686 — NON-ROOT ONLY (plan verrouillé, à faire)
> Décision 2026-06-22 : cible **non-root uniquement** ; chemin root abandonné.
> Plan détaillé : commentaire #685.
- [ ] **VpnService in-app** (`com.wireguard.android:tunnel` / GoBackend wireguard-go)
— l'APK EST le client WG, plus de Play Store, détection tunnel in-app fiable.
- [ ] **CA en DER** (fix « nom de cert vide » du KeyChain intent) + `network-security-config`
pour que la WebView in-app fasse confiance au CA ca-wg.
- [ ] Retirer RootShell/RootOnboard/BootReceiver ; manifest VpnService + consent VPN.
- [ ] Limite Android : pas de CA **système** sans root → MITM système impossible ;
surface « safe browsing » = WebView in-app. À documenter.
- [ ] Build via CI `build-android-apk` + **test sur appareil** (gros build, itératif).
---
## 🎯 Backlog priorisé — revue 2026-06-24 (64 issues ouvertes)
> Index d'autorité du triage. Les sections « Phase X » plus bas sont historiques :
> plusieurs portent « ✅ COMPLETE » alors que l'issue est restée **ouverte** (livré
> mais jamais fermé) → marquées **[vérifier→fermer]** ci-dessous.
### 🔴 T0 — Régressions & bugs sécurité (petits, débloquants, CSPN priv-sep)
- #494 secubox-core ExecStart écrase tmpfiles.d `/run/secubox` *(worktree actif)*
- #468 `/etc/secubox` parent 0750 casse la traversée non-secubox *(régression récurrente)*
- #471 secubox-mesh postinst écrase perms `/run/secubox` *(régression)*
- #421 sockets `/run/secubox` cachés en mount-ns privé (RuntimeDirectory)
- #447 kiosk : mot de passe admin semé par le CI (users.json shippe un hash) **← fuite**
- #91 haproxyctl régénère haproxy.cfg avec `waf_inspector` inexistant *(intégrité WAF)*
- #65 nginx : routes API manquantes dans webui.conf
- #53 Wazuh uvicorn 100% CPU spin
- #121 metablog ingest : dirs en `secubox:secubox`
### 🟠 T1 — Plan d'enforcement sécurité (mission CSPN ; détection→action)
- #498 Phase 7 — WAF active enforcement (mitm→CrowdSec→nft drop) *(worktree actif)*
- ✅ #519 Phase 13 — enforcement plane **FERMÉ 2026-06-22** (livré + réparé :
blacklist-sync avortait sur NXDOMAIN + timeout unit → fix `|| true` +
TimeoutStartSec 600 ; vérifié live, default-off). Inclut 13.B #522.
- #455 secubox-egress — détection egress + corrélation RDS multi-signaux
- #500 Phase 8 — Utiq operator-grade tracking (detect/alert/bypass)
- #514 Phase 12 — plateforme anti-human-detection (parent ; sous-tracks fermés)
- ✅ #515 Phase 12.A CDN cache detection — **FERMÉ** (live, `social_host_meta.cdn_vendor`)
- ✅ #516 Phase 12.B anti-bot detection — **FERMÉ** (live via #564/#565, `social_antibot`)
- #525 Phase 14 — plan de déception (idée future, parké)
- ⬜ Suivi #519 perf (non bloquant) : DNS-guard ne résout que les 2000 premiers
domaines/cycle (5523 en base) → couverture partielle ; résolution séquentielle
lourde sur board saturé. Option : résolution parallèle bornée + rotation du cap.
### 🟡 T2 — UX / Hub / conscommateurs report (worktrees actifs + polish)
- #615 security-posture dans la sidebar Hub *(worktree actif)*
- #655 webext content-script banner CSP-immune *(worktree actif)*
- #485 toolbox SOC scoring *(worktree actif)*
- #513 ToolBox WebUI : sous-onglets + retrait UI /admin redondante
- #69 diagramme flux trafic responsive
- #67 cache history-aware glances/netdata
- #68 health checks + dépendances services au démarrage
### 🟢 T3 — Backlog feature (valeur, non bloquant)
- #685 APK 'corrupt' — CI signe avec clé éphémère *(plan APK verrouillé)*
- #686 android-toolbox flux non-root cassé *(plan APK verrouillé)*
- #429 nextcloud dashboard : API stubs au lieu de la vraie instance *(bug)*
- #430 nextcloud — fédération OCM (doc/outillage)
- #472 nextcloud — Gondwana Desktop (canvas + widgets)
- #592 secubox-webmail-hub (Gmail OAuth2 + Gandi + OVH)
- #66 auth Google OAuth
- #70 Health Banner System *(preplanned)*
- #71 CDN proxy injection *(preplanned)*
- #393 source-home des scripts health prober
### 🔵 T4 — Hardware-gated (dépend de pièces ; piste parallèle ; pas de spare EP06)
- Modem/PCIe : #254 modules kernel LTE · #255 pins mPCIe modem · #460 DTS cp0_pcie2 ·
#467 U-Boot comphy5 SerDes · #462 pivot HW AR9271/MT
- Mesh/BLE : #449 WiFi 802.11s · #452 BT mesh · #453 QR multi-canaux · #454 sourcing BLE 5.x
- GSM : #347 sentinelle-gsm
- Smart-Strip : #33 module HMI · #42 sous-repo · #379 packaging
- Eye-remote : #41 sous-repo · #79 buildroot · #127 variante square · #138 radar_concentric ·
#155 collision link-rename *(bug)* · #158 multi-gadget L3 · #478 métriques live Round Eye
- VILLAGE3B : #480 dossier presse · #497 poster grand public
### ⚪ T5 — Images / OS variants (basse urgence)
- #446 Full Traveller OS multi-mode/arch · #125 build-live-usb +virtualbox · #422 vm-x64 cascade
### ⚫ T6 — Docs / housekeeping
- #81 headers SPDX CMSD-1.0 partout · #243 clarifier scope secubox-zkp-auth *(question)*
- #474 ToolBoX (epic parent — garder comme tracker)
--- ---
## 🔥 P0 — Immediate (in flight) ## 🔥 P0 — Immediate (in flight)
### kbin Tor endpoint — anonymized quick-switch surfing (#683)
> Capstone du couteau suisse cyber : l'anonymat de la sortie. Spec :
> `docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md`.
> Invariants : inspection préservée, fail-closed, opt-in (défaut OFF), no DNS leak, CSPN audit.
- [ ] **Transport** — Option A dialer SOCKS5 upstream (cœur Go #662, *préféré*) vs
Option B nft mark → Tor TransPort (fallback pré-#662).
- [ ] **Profil Tor egress** — réutiliser `secubox-exposure` (bootstrap/NEWNYM), egress-only.
- [ ] **API toolbox**`POST /admin/tor/{on,off}` (WG-hash scoped) + `GET /tor/state` +
`POST /tor/newnym` + état SQLite per-client (TTL 24h).
- [ ] **UI kbin** — toggle 🧅 + badge état + flag pays de sortie + bouton « nouvelle identité ».
- [ ] **Leak-guard nft** + DNS-over-Tor (test exit IP + resolver ≠ Unbound).
- [ ] **`tls_splice` OFF en mode Tor** (#649) — sinon les flux asset fuient l'IP réelle.
- [ ] **CSPN** — audit-log chaque bascule ; soak DARK (flag présent, UI cachée) avant flip.
### ToolBox clients (`clients/`)
- [x] **#531 Android scaffold + CI** — Gradle/Compose one-tap onboarding,
debug APK via `build-android-apk.yml`. CI green.
- [x] **#536 serve APK from toolbox** — `GET /wg/toolbox.apk` + onboard button +
`secubox-toolbox-fetch-apk` helper.
- [x] **#538 Android root-mode silent** (PR #539) — system CA install + native
kernel WireGuard + auto R3 verify, gated behind explicit root tap.
- [x] **#532 browser extension** (`clients/webext-toolbox/`) — MV3 Firefox
`.xpi`/Chromium; live tracker badge + popup mini Round-Eye graph over
`/social/*`; `GET /wg/toolbox.xpi` + fetch helper + `build-webext.yml`.
- [x] **#532 release** — tag `webext-v0.1.1` published the `.xpi`
(downloadable, verified 200). `make_latest:false` + tag-pinned URL so it
doesn't steal "Latest" from the Android APK release.
- [ ] **release signing** — Android keystore + AMO `.xpi` signing secrets in CI
for stable published fingerprints (currently unsigned sideload).
- [ ] **#532 follow-ups** — optional `GET /social/live/{token}` SSE (replace the
client-side poll) ; Poke/Emancipate per-site control once #525 (deception)
ships ; Chromium PNG icon rasterisation for the Web Store.
### Phase 13 — Protection enforcement plane (#519) — ✅ COMPLETE
- [x] **13.A spine** (#521, `2.6.8`, v2.13.17) — nft blacklist set + forward-drop
chain + sync (CrowdSec + threat-intel). + override_dh_strip drift fix.
- [x] **13.B DNS-guard** (#522, `2.6.9`, v2.13.17) — résout domaines blocklistés
→ IPs ; détection DoH/DoT (block opt-in).
- [x] **13.C attribution** (#524, `2.6.10`, v2.13.18) — per-device blocked-attempts
+ quarantine + endpoints + tile.
- [x] **13.D feedback** (#527, `2.6.11`, v2.13.19) — escalation evaluator
(detections→nft/cscli/quarantine), audit-log, **default OFF**.
- [ ] **13.x opt-in tuning** — activer `SECUBOX_ESCALATE_*` / `SECUBOX_DOH_BLOCK`
selon politique opérateur quand voulu.
- [ ] **threatfox feed = 0** — investiguer l'ingestion domain vide (impacte
13.B resolved_domains).
### Phase 14 — Plan de déception (#525, idée future)
- [ ] Pseudo-réponses proxy au lieu de blocage IP (indistinguable, pollue
le profil) + neutralisation des scripts CDN préchargés. R3 consenti,
réutilise la détection Phase 11/12. **Pour plus tard.**
### Phase 11 — Social mapping per device (#502) — ✅ COMPLETE (v2.13.15) ### Phase 11 — Social mapping per device (#502) — ✅ COMPLETE (v2.13.15)
- [x] **11.A backend** (#505, `2.6.0`) — correlation engine + SQLite + API. - [x] **11.A backend** (#505, `2.6.0`) — correlation engine + SQLite + API.
@ -237,9 +21,9 @@
graph + by_cdn. Mergé. graph + by_cdn. Mergé.
- [x] **12.B anti-bot** (#516, `2.6.5/2.6.6`) — detect_antibot (détection - [x] **12.B anti-bot** (#516, `2.6.5/2.6.6`) — detect_antibot (détection
seule) + ring levels visibles + Carto/Reset opérateur. Mergé. seule) + ring levels visibles + Carto/Reset opérateur. Mergé.
- [x] **12.C opérateur-grade / state-adjacent** (#518, `2.6.7`, v2.13.16) — - [ ] **12.C opérateur-grade / state-adjacent** — étend #500 Utiq :
detect_operator_grade (telco MSISDN/x-acr + consortium Utiq/TrustPid + identité carrier-grade (MSISDN injection, CGNAT fingerprint) + analytics
data-broker LiveRamp/BlueKai/Palantir-class). Top-severity lens + PDF. state-adjacent. Prochain track.
- [ ] **12.B bypass** — résolution de challenge (gated derrière doctrine - [ ] **12.B bypass** — résolution de challenge (gated derrière doctrine
lawful-use + design review ; R3 opt-in uniquement). lawful-use + design review ; R3 opt-in uniquement).
- [ ] **12.D noise counter-measures** — cookie-noising / header-strip / - [ ] **12.D noise counter-measures** — cookie-noising / header-strip /

View File

@ -1,430 +1,5 @@
# WIP — Work In Progress # WIP — Work In Progress
*Mis à jour : 2026-07-01* *Mis à jour : 2026-06-10*
---
## ✅ 2026-06-30 → 07-01 : Substrat de confiance Gondwana — fédération + registry + macros (#766 #769 #771)
Trois features complètes (brainstorm → spec → plan → SDD subagent-driven avec revue
adversariale), **toutes mergées sur master**, déployées gk2 + c3box, prouvées live :
- **#766 — annuaire fédération sans-confiance (0.2.0→0.3.x)** ✅ CLOSED/master.
`ingest_offer` impose `did_from_pubkey(pubkey)==provider` avant la vérif sig
(auto-certifiant, aucune confiance préalable) ; offres portent `sig`+`signer_did`+
`provider_pubkey` ; verbe `genesis()` + CLI `annuairectl` (init/whoami/status/offer/
services/pull) ; écouteur mesh (postinst, IP-mesh only, `ip_nonlocal_bind`, validate-or-
revert). Live : un 2e nœud (fondateur distinct) `annuairectl pull` → ingest sans-confiance.
- **#769/#770 — p2p Service Registry = vue live du catalogue annuaire (secubox-p2p 1.8.0)** ✅
MERGED. `/services` fusionne catalogue annuaire + abonnements + overlay d'activation +
services p2p-locaux ; « Auto register all » (active locaux + s'abonne aux distants selon
auto/pending) ; s'abonne EN TANT QUE nœud (clé 0600). Live gk2+c3box.
- **#771/#773 — sous-système macro + tor-exit (secubox-macro 0.1.0 NEW, p2p 1.9.0, annuaire 0.3.3)** ✅
MERGED (+#772 auto-fermé). Un service propose une **macro d'accès** vettée, confinée
AppArmor : `macroctl` dispatcher root (allowlist kind, tamper-guard plugin, euid env-pin,
audit append-only) + `macros.d/tor-exit` (nft SOCKS-over-mesh grant/revoke) + sudoers
(env_reset) + auto-détection table firewall (`secubox_filter`|`filter` via
`/etc/secubox/macro.conf`). Endpoint grant p2p (auth Subscription auto-signée, self-cert,
auto-mode). **Démo live end-to-end** : gk2 propose son exit Tor → fédère → c3box s'abonne+
active → pull grant sur le mesh → gk2 nft-autorise l'IP mesh de c3box → **c3box route via
l'exit Tor de gk2** (`IsTor:true`). La boucle de revue SDD a attrapé ~10 Criticals avant merge.
### ⬜ Next Up (déféré, non bloquant)
- **Liaison NIZK/PSI GK·HAM** — les verbes annuaire utilisent encore les stubs documentés
(`ZKP-HAM-v1`) ; brancher `zkp-hamiltonian` cffi.
- **Nouveaux kinds macro**`wg-relay`, `dns-resolver`, `http-mirror` (chacun = un plugin
`macros.d/<kind>` vetté + profil AppArmor, même framework).
- **Macros en mode `pending`** — nécessite la fédération cross-nœud des Subscription/APPROVE.
- **Mesh gk2→c3box (sens inverse)** — pull satellite→master OK ; master→satellite bloqué
(nft c3box) ; + installer Tor sur c3box pour un provider tor-exit natif.
---
## ✅ 2026-06-27 : c3box → SecuBox Debian — première install réussie · netboot prouvé (#748 #737)
### ✅ Fait (session 2026-06-27)
- **Netboot gk2→c3box prouvé** — factory U-Boot 2020.10 → TFTP → rescue shell installeur
(kernel 6.12.85 #5secubox). Détour cabling résolu (impasse LAB, pas logiciel).
- **Première install SecuBox Debian sur un MOCHAbin physique (c3box)** — image CI artefact
`secubox-mochabin-bookworm` (run 27426515472, 8 Gio), SHA256 + signature vérifiés,
`gunzip|dd` en RAM → eMMC. c3box boot Debian v1.9.0 avec stack complète.
- **boot.scr workaround déployé** — extlinux.conf charge le kernel à `0x02080000` (réservé
factory U-Boot → reset). Construit `/boot/boot.scr` (kernel@`0x0a000000`) ; auto-boot
Debian sans intervention vérifié après reboot.
- **#748 bloquant documenté** — ciseau U-Boot : mochabin board UNIQUEMENT dans fork Tow-Boot
2022.07 (pas de `wget`) ↔ `wget` UNIQUEMENT dans stock ≥2023.07 (pas de board mochabin).
Branche `feature/748-enhanced-tow-boot-http-netboot-serial-fl` parkée (spec+CI+Kconfig en
place, dépend du backport wget OU port board mainline).
### ⬜ Rig netboot temporaire gk2 à démonter (quand c3box autonome)
- `lan1=192.168.77.1/24` avec dnsmasq DHCP + `nft iif lan1 accept` + nginx `:8099` encore actifs.
- À retirer une fois c3box en prod (voir TODO T5 — teardown rig).
### ⬜ Bootloader propre à faire (#748 ou alternative)
- boot.scr = workaround ; fix durable = enhanced Tow-Boot (#748, bloqué ciseau) OU corriger
les adresses de boot dans l'image (extlinux.conf → `0x0a000000`). Voir TODO T5.
---
## 🗂️ 2026-06-22 : triage issues (30 ouvertes → revue obsolètes)
- **Fermées (user-validé 2026-06-22)** : #722 (nDPId — décidé contre, reverté) ·
#475 ToolBoX Phase 1 (live 2.7.x) · #502/#507/#508 Social mapping (carto +
/social/me + report PDF live) · #495 Phase 5 mitm-LXC (superseded par #662 Go
sbxmitm host) · #531 APK one-tap (superseded par #685/#686 non-root) ·
#486 geoip/ASN+flags+catégories dans rapports (livré master : geo.py + dpi_class.py +
report wiring ; complémentaire de #718 ASN collector ; worktree stale nettoyé) ·
#515 CDN detection (live `social_host_meta.cdn_vendor`) · #516 anti-bot detection
(live via #564/#565) · #519 enforcement plane (livré + **réparé** : blacklist-sync
avortait NXDOMAIN + timeout unit → fix `|| true` + TimeoutStartSec 600, vérifié live,
default-off ; inclut #522). Toolbox source bumpé 2.7.18 (fix live-patché sur gk2) ·
#468 /etc/secubox traversal (source+live = 0755, secrets/CA enfants restent 0750).
- **Actives (worktrees en cours)** : #655 webext banner · #615 security-posture ·
#494 secubox-core ExecStart · #498 Phase 7 WAF enforcement · #485 SOC scoring.
### 🔎 Reco T0 — recon live gk2 2026-06-24 (avant fix)
- ✅ **#494** : **FIX SYSTÉMIQUE poussé** (`fix/494-…`). Pas que core : 7 units re-chownaient
le parent partagé `/run/secubox` (core+hub services, eye-remote/eye-square/metablogizer/
metrics/p2p postinsts ; eye-square chownait aussi /var/log/secubox = pire). Tous nettoyés
(mkdir fallback only ; logs modules en sous-dossier propre ; orphan /etc/tmpfiles.d nettoyé).
**Vérifié live** : /run/secubox 1777 **root:root** stable après restart core ET hub ; webui 200.
Bumps core 1.1.7/hub 1.4.4/eye-remote 1.0.1/eye-square 1.0.4/metablog 1.2.2/metrics 1.0.4/p2p 1.7.1.
- ✅ **#471** (mesh /run/secubox) : déjà résolu (changelog mesh "drop install -d /run/secubox") → verify-close.
- ⬜ **#421** : sockets cachés en mount-ns privé (RuntimeDirectory) — mécanisme distinct, non traité.
- 🆕 Suivi (classe #511) : mesh/toolbox/admin font `install -d -o <module> /var/log/secubox`
(propriétaire du parent partagé = user module) → autres daemons ne peuvent créer leurs logs.
Séparé de #494, à traiter (sous-dossiers propres comme fait pour eye-square/p2p).
- **#447** : pas une fuite — `password_hash=null` → lockout kiosk + user CI parasite ;
**CI-image-gated** (rpi400, pas gk2).
- **#91** : `haproxy.cfg` active valide ; backup `*.broken-by-haproxyctl-*` prouve le bug
passé ; drift-guard #627 rattrape. Root cause = generate `haproxyctl` (api/main.py l.846/896).
- ✅ **#53** : **FIX poussé** (`fix/53-…`) — gate `ConditionPathExists=/var/ossec/etc/ossec.conf`
+ `RestartSec=5` ; module conservé (SIEM opt-in). Vérifié gk2 (/var/ossec absent). Bump 1.0.1.
- ✅ **#65** : déjà résolu en prod (webui.conf déployé inclut `secubox-routes.d/*.conf`,
163 snippets). Template `common/nginx/webui.conf` (stale) synchronisé sur `feature/65-…`.
Reco fermer. Convention : `secubox-routes.d/`=actif, `secubox.d/`=legacy.
- ✅ **#121** : **FIX poussé** (`fix/121-…`) — helper `fix_perms` chown -R secubox:secubox
le site dir après chaque ingest .git (metablog-ingest-site.sh). Script dev, pas de deploy.
- ⬜ Restent : **#91** (deploy WAF risqué) · **#65** (refactor include, risque 502) ·
**#447** (CI kiosk) · **#494/#471/#421** (worktree fix/494). Build+deploy toolbox 2.7.18 (#519) en attente.
- **Backlog/future** : #685/#686 APK non-root (plan verrouillé) · #592 webmail-hub ·
#514/#515/#516/#519/#522/#525 Phase 12-14 (#515 CDN / #516 anti-bot partiellement
couverts par antibot_sites/opgrade_sites du social graph) · #500 Utiq · #497/#480/
#478 VILLAGE3B Eye/poster · #472/#430/#429 Nextcloud · #471/#468/#421 perms (à
vérifier si déjà corrigées) · #467/#462/#460/#255/#254 hardware/kernel · #455 egress ·
#454/#453/#452/#449 mesh/BLE · #448/#447/#446/#434 kiosk · #422 vm cascade ·
#393/#379/#347 packaging · #513 WebUI sub-tabs.
- ⚠️ Fermeture finale = **user only** (sauf issues créées en session) ; les
recommandations ci-dessus sont commentées sur chaque issue.
---
## ✅ 2026-06-22 : DPI exfil + Netrunner report + sbxmitm fixes (tous mergés, live gk2)
Session livrée intégralement sur master + déployée. Détail dans HISTORY 2026-06-22.
### ✅ Fait (mergé + live)
- **DPI exfil pipeline (#687)**`secubox-dpi 1.1.2` : flowcap (ndpiReader) → Go
collector (catégories cloud/media/game/adult/ai/messaging/filehost/social + scénarios
exfil) → `/api/v1/dpi/exfil` ; dashboard "Cloud Exfiltration Watch" + cartes repointées ;
beaconing tuné (#692) ; cumulatif 7j `cumulative.json` (#705) ; packagé arm64.
- **Report kbin = fiche Netrunner (#707)** — HTML (onglets Pistage/DPI/Overall + persona
néon) **et** PDF (`_persona_block` + "En un coup d'œil" + grille donuts + carto + tables
emoji). Charts en **PNG matplotlib** (#714, rendu universel iOS/Chrome) ; grille = une
image 2×2 (#716, fin des 24 pages). Classe via UA live + niveau R3 auto (wg peer).
- **sbxmitm** — cert forgé 24h→365d (#689, fin des "certificat expiré") ; fin de la
troncature >8MiB (#697, Gmail OK) ; splice own-domain **rejeté** (#688, on intercepte tout).
### ⬜ Next Up (différé)
- **#685/#686 APK on-device — NON-ROOT ONLY (plan verrouillé)** : VpnService in-app
(wireguard-go), CA en DER + network-security-config WebView, retrait du chemin root.
Gros build Android (CI + test device) → session dédiée. Détail : commentaire #685 + TODO.
- **DPI Phase 3** — ✅ enrichissement ASN (#719, 1.1.3) · ✅ historique + timeline
(#721, 1.1.4) · ❌ démon nDPId **écarté** (#722/#723 revertés) : risque perf
(démon permanent vs fenêtres ndpiReader bornées) sur board saturée → **on garde
ndpiReader**. **Phase 3 close.**
- **#685 APK on-device** — install auto CA + handoff WG + détection tunnel (en attente
décision rooted vs non-root du user).
- **Cosmétique PDF** — glyphes drapeaux régionaux dégradent en lettres (police embarquée) ;
chiffres légèrement espacés dans certaines cellules. Non bloquant.
---
## 🔄 2026-06-19 : kbin Tor egress (#683) — ToolBoX 2.7.1, implémenté DARK
Switch + tunnel Tor quick-switch livrés sur `feature/683`, **défaut OFF / fail-closed**.
Détail dans la section "Implémenté DARK" ci-dessous + HISTORY 2026-06-19.
---
## 🔄 2026-06-19 : kbin milestone — ToolBoX 2.7.0 + chapitre Tor (plan)
Checkpoint de fin de session. Pas de changement de comportement runtime — docs +
positionnement + version + plan de la lame suivante.
- ✅ **ToolBoX 2.7.0** (middle release) — clôt la ligne 2.6.x (ad-intelligence /
Anti-Track v2 / anti-bot uTLS #662), ouvre le chapitre kbin « premier outil du
couteau suisse cyber ». kbin = perf transparente + full encrypted + poison/smog +
bandeau anti-adware + safe browsing.
- ✅ **Docs kbin** — wiki [`Kbin-Toolbox.md`](../docs/wiki/Kbin-Toolbox.md),
[`FAQ-KBIN-TOR.md`](../docs/FAQ-KBIN-TOR.md), blurb README.
- ✅ **Plan #683** — spec
[`2026-06-19-kbin-tor-anonymized-surfing-design.md`](../docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md) :
endpoint Tor quick-switch (egress sortant, fail-closed, opt-in, no DNS leak,
inspection préservée). Dépend du cœur Go #662.
### ✅ Implémenté DARK — chapitre Tor (#683, ToolBoX 2.7.1, branche feature/683)
- ✅ **Transport tranché** : *torify l'egress MITM* (owner-match nft sur l'uid
`secubox-toolbox`/mitm-wg → Tor TransPort 9040 / DNSPort 5353). Inspection
préservée. Décision USER (vs dialer SOCKS5 #662 = bloqué, vs torify client = casse
l'inspection).
- ✅ **Switch** : flags `tor_mode`/`tor_preset` (filters.json) ; API kbin-gated
`GET/POST /admin/tor/{state,on,off,newnym,check-leaks}` ; onglet 🧅 WebUI (badge,
toggle, NEWNYM, sonde fuite). `tor_ctl.py` réutilise le control-port de secubox-tor.
- ✅ **Tunnel** : `conf/nft-toolbox-tor.nft` (fail-closed kill-switch + drop v6) +
`conf/torrc-toolbox-egress.conf` + reconciler root path-triggered
(`secubox-toolbox-tor.path` surveille filters.json → portail reste
NoNewPrivileges=true). nft chargé AVANT tor (pas de fenêtre clearnet).
- ✅ 166 tests verts ; license headers OK ; changelog 2.7.1.
#### ⬜ Avant flip ON (USER)
- Soak DARK puis `tor_mode=true` via l'onglet (admin.gk2).
- Test de fuite **hors-board** : l'IP réelle de la box ne doit jamais apparaître.
- Forcer `tls_splice` (#649) OFF quand armé (sinon flux asset fuient l'IP réelle).
- **Per-client (WG-hash)** : nécessite le dialer SOCKS5 du cœur Go #662 (l'owner-match
est global). Suivi sous #662.
---
## 🔄 2026-06-17/18 : Anti-Track v2 + perf/ops sprint (gk2 live)
Tout mergé sur master + déployé sur gk2. Détail dans HISTORY 2026-06-18.
- ✅ **Anti-Track v2 (#633, PR #637)** — bloque/empoisonne/anonymise, moteur
`privacy.py` + addon `privacy_guard.py`, learning (`learn.py`), IP-drop +
unbound DNS-refuse (`ip_dns.py`/`escalate.py`), bypass-seed + #filtres badges,
#social top-5. **Tourne DARK** (`privacy_enforce` unset). Wiki `Anti-Track.md`.
- ✅ **Banner saga (#636/#639, PR #638/#640)** — mitm sert loader/bundle pour
toute origine (PeerTube fixé), CSP fallback, top-bar, 1 bannière/visite.
- ✅ **#634/#635** — reset-all clients + emojis device/flag/hosting.
- ✅ **#642 (PR #643)** — social-graph ignore les edges IP-littéraux ; KPI
"Trackers vus" = table.
- ✅ **#644 (PR #645)** — hub dashboard/health-batch servis depuis cache TTL
(health-batch 3.3 s → 8 ms) ; clients/rich enrichit 12 max. **hub 1.4.6**.
- ✅ **#646 (PR #647)** — adaptive Accept-Encoding strip : plus de pages
CSP-strict tirées décompressées via le worker R3 GIL-bound. **toolbox 2.6.53**.
- ✅ **crowdsec** réparé (403 transitoire CDN → `dpkg --configure` RC=0, audit clean).
- ✅ **#623 (PR #648, merged 9950e9ec)** — clobber systémique RÉSOLU au source.
La vraie cause : boilerplate scaffold `install -d -m 750 /var/lib/secubox` +
`/run/secubox` (parents NUS) dans ~56 postinsts — écrit `-m 750` (3 chiffres),
d'où le ratage des sweeps précédents. Empiriquement prouvé que le form
`install -d -m 750 /parent/leaf` NE clobbe PAS le parent (seuls les targets
parents-nus). Fix : tous → 1777 (/run) / 0755 ; 6 lignes multi-arg splittées
(4 mettaient /var/lib en world-writable 1777) ; 3 `chmod 750 /var/log` ;
scaffold `new-package.sh` + `PATTERNS.md` ; core 1.1.8 tmpfiles.d déclare les 5
parents 0755. **PAS de mass-deploy** (60 paquets = mass-restart = risque
thundering-herd) ; live couvert par `dirs-guard.timer` ; arrive au prochain
build CI / reflash.
- ✅ **#649 Lever A — selective SNI-splice (PR #650, toolbox 2.6.54 LIVE dark)**.
New `tls_splice` addon (first in mitm-wg chain) splices pure-asset flows at the
TLS ClientHello — curated media seed (googlevideo/ytimg/fbcdn/twimg/scdn…)
autolearn-promoted never-HTML hosts — so GIL-bound R3 workers skip
forge/decrypt/parse/16-addons on no-L7-value flows. Ships `tls_splice=observe`
(DARK: classify+log, still MITM). Deployed gk2, addon loads clean, 0 runtime
errors. Answer to "do we need full mitm?": YES for outbound HTTPS (per-host cert
forging is intrinsic) — but only decrypt what we modify. Lever B (Go/Rust core)
= strategic follow-up. WAF = later.
### ⬜ Next Up
- **#649 SOAK → FLIP** — review `would-splice` logs + `/run/secubox/splice.json`
on real traffic for a soak window, confirm no first-party/HTML host is
classified, then flip `tls_splice=on` in `/etc/secubox/toolbox/filters.json`
(hot-reload). Before flip: the fortknox-via-WebUI refresh gap is already fixed.
- **Lever B (#649 follow-up)** — Go/Rust forging-proxy core if A isn't enough.
- **Anti-Track v2 ARMING** (décision USER, gated) — soak observe-only puis flip
`privacy_enforce=true` ; régénérer `data/cdn-allowlist.txt` depuis les plages
publiques avant `privacy_ip_drop` ; `unbound-checkconf` avant `privacy_dns_feed`.
- **Tunnel R3 perf** — l'encoding fix aide ; reste la contention CPU board-wide
(load ~5/4 cœurs, workers mono-thread). Lever suivant = réduire les co-tenants
(gitea/R2-mitm/crowdsec/metrics) ou isoler le mitm, pas du tuning d'addon.
- **#615** — Security Posture dans la navbar du Hub (petit enhancement).
- **#592 webmail-hub** — BLOQUÉ : besoin client OAuth Google + vhost ; Phase 1
IMAP (Gandi/OVH) peut démarrer sans OAuth.
---
## 🔄 2026-06-14 : ToolBoX privacy/perf sprint — 2.6.36 live (see HISTORY)
Tout mergé + déployé sur gk2 (kbin sain, `secubox-toolbox 2.6.36`).
Détail complet dans HISTORY 2026-06-14. Résumé :
- ✅ Protective spoof (#560), modular filters + ad-ghoster (#566, collapse
#584), media cache opt-in (#577), autolearn (#589/#591), DPI media donut
(#570), donut + domain-nugget cartographie (#553/#587, IP cachées #575,
favicons #555), guirlande banner + pin (#572/#578), webext popup panel
(#574), /ca/fingerprint R3 (#562), postinst restart fix (#581),
detect_antibot deployment-vs-challenge (#564).
- ✅ Clients : APK v0.3.0 (zero-tap launch+boot), webext v0.1.4.
- ✅ Fixes live : Nextcloud iPhone photos (files_antivirus off + PHP
limits), kbin 503 (#581).
### ⬜ Next Up
- **#592 secubox-webmail-hub** (Gmail OAuth2 + Gandi + OVH, inbox unifié) —
design filé, **BLOQUÉ** : besoin d'un client OAuth Google (client_id/
secret/redirect) + nom de vhost + (read-only Phase 1 ?). Phase 1 IMAP
(Gandi/OVH) peut démarrer sans OAuth sur "start phase 1".
- Côté user : re-trust R3 CA `D5:E4:3A` sur l'iPhone (bannière HTTPS) ;
tester l'upload photo Nextcloud ; activer `media_cache` si voulu
(`/admin/filters/ui`) et surveiller `/admin/cache`.
---
## 🔄 2026-06-13 : Browser extension — emancipate cartographie live (#532)
Extension navigateur (`clients/webext-toolbox/`, MV3 Firefox `.xpi` +
Chromium) sœur de l'app Android. Sort la *cartographie sociale* R3 dans
le navigateur : badge live des traceurs + popup.
- **Extension** : `manifest.json` (MV3, background `service_worker` +
`scripts` pour FF115+/Chromium), `api.js` (client `/wg/r3-check`,
`/social/me` → token, `/social/graph/{token}`, `/social/wipe`),
`background.js` (badge = total_trackers, re-pair silencieux si token
expiré, couleur escalade gold→anti-bot→opérateur), popup (4 tuiles
stats + **mini Round-Eye graph SVG sans dépendance** + top-traceurs
taggés CDN/anti-bot/opérateur + actions cartographie/PDF/RGPD-wipe),
options (hôte/fenêtre/token manuel). Pas de CORS backend nécessaire
(host_permissions). Validé : JSON+JS+SVG OK, `.xpi` build 11.8 KB.
- **Serve depuis la toolbox** (`2.6.14`) : `GET /wg/toolbox.xpi` (local
sinon 302 → release), bouton `🧩 Extension navigateur` sur les 2
panneaux onboard, helper `secubox-toolbox-fetch-xpi`, postinst dir.
- **CI** : `build-webext.yml``web-ext lint` (0 erreur, 2 warnings
bénins) + build, artifact, release asset sur tag `webext-v*`.
- **Release** (PR #540 + #541, mergées) : tag `webext-v0.1.1` poussé →
CI a publié `secubox-toolbox-webext.xpi` (téléchargeable, vérifié 200).
`make_latest:false` + URL **tag-pinned** dans `/wg/toolbox.xpi` +
`secubox-toolbox-fetch-xpi` pour ne pas voler le pointeur "Latest" à la
release APK Android (dont l'endpoint résout via `/releases/latest/...`).
→ bumper le tag dans la constante + le helper à chaque `webext-v*`.
- **Reste à faire** : signature AMO (`.xpi` non signé = sideload/dev) ;
endpoint SSE `/social/live/{token}` optionnel ; icône PNG Chromium ;
contrôle Poke/Emancipate par-site quand #525 (déception) arrive ;
déployer `secubox-toolbox 2.6.14` sur la board pour activer le bouton.
---
## 🔄 2026-06-13 : Android ToolBox app — serve + root-mode silent onboarding (#531/#536/#538)
App compagnon Android **one-tap R3** pour la cabine VILLAGE3B
(`clients/android-toolbox/`, `in.secubox.toolbox`, Kotlin + Compose).
- **#531 — scaffold + CI** : projet Gradle/Compose (5-step stepper
Discover→InstallCa→ImportProfile→Verify→Done), client `HttpURLConnection`,
workflow `build-android-apk.yml` (debug APK artifact, release asset sur
tag `android-v*`). CI **GREEN**.
- **#536 — serve depuis la toolbox** : endpoint `GET /wg/toolbox.apk`
(sert le build local `/var/lib/secubox/toolbox/android/`, sinon 302 →
release GitHub) + bouton *📱 Installer l'app ToolBoX (1-tap)* dans les
panneaux onboard kbin + helper `secubox-toolbox-fetch-apk`. Vérifié :
200 `application/vnd.android.package-archive`, 14.8 MB.
- **#538 — root-mode silent onboarding** (PR #539, branche poussée) :
bouton *⚡ Installation automatique (root)* sur devices rootés →
install CA dans le magasin **système** (bind-mount cacerts + APEX
conscrypt, SELinux ctx, `subject_hash_old` en Kotlin pur) + tunnel
WireGuard **natif noyau** (`ip link add … type wireguard` + `wg set`) +
vérif R3 auto. Fallback handoff app WireGuard si noyau sans WG. Toutes
les actions root gated derrière le tap explicite. Nouveaux fichiers
`RootShell.kt`, `RootOnboard.kt`, step `RootAuto` (log streamé).
- **Reste à faire** : release signing (keystore secret CI) pour une
empreinte publiée stable — actuellement debug-signé (sideload).
---
## 🔄 2026-06-12 : Admin WireGuard tunnel + SSH hardening (ref #529)
Accès admin out-of-band + fermeture de la surface SSH publique.
- **wg-admin** (#529) : interface dédiée UDP **51821**, server `10.98.0.1/24`,
distincte de wg-toolbox (51820). Peer `gandalf-admin` @ `10.98.0.2`.
nft drop-in `secubox-admin-wg.nft` (udp/51821), `wg-quick@wg-admin`
enabled. Client importé dans NetworkManager du poste dev, tunnel UP,
`ssh root@10.98.0.1` confirmé (key auth).
- **Découverte sécurité** : la box subissait un brute-force SSH public
actif (centaines de tentatives 87.251.64.x / 51.68.34.x + IPs déjà
blacklistées). Le routeur `192.168.1.254` port-forward :22 → box sur
`eth1`/`lan0`, et l'input chain a un blanket `iif eth1 accept` (le
DNAT préserve l'IP source publique réelle).
- **Hardening appliqué + vérifié** :
- sshd : `PasswordAuthentication no` + `PermitRootLogin prohibit-password`
(drop-in `99-secubox-hardening.conf`, key-only).
- nft SSH-guard : `tcp dport 22 ip saddr != { 192.168.1.0/24, 10.0.0.0/8 } drop`
inséré AVANT `iif eth1 accept` (live sans flush + persisté dans
`/etc/nftables.conf`).
- Résultat : `ssh root@10.98.0.1` (tunnel) OK key-only ; public
`admin.gk2.secubox.in:22` **timeout (bloqué)**. Tables blacklist/wg
intactes.
- **Script reproductible** `scripts/setup-admin-tunnel.sh`
(`provision | add <name> | harden`), idempotent, branche `feature/529`
poussée (pas de PR).
- **Reste à faire (côté user)** : retirer le port-forward :22 du routeur
(le tunnel remplace l'accès) ; IPv6 SSH non couvert par le guard v4
(à ajouter si exposition IPv6).
---
## 🔄 2026-06-11 : Phase 12.C + Phase 13 COMPLETE (protection enforcement plane) — v2.13.16→19 (ref #518-#528)
### ✅ Phase 12.C — operator-grade / state-adjacent (#518, v2.13.16)
detect_operator_grade : telco header-enrichment (MSISDN/x-acr), consortium
(Utiq/TrustPid), data-broker / state-adjacent (LiveRamp/BlueKai/Acxiom/
Neustar/Tapad/Experian/Palantir-class). Top-severity void-purple lens +
PDF section. `secubox-toolbox 2.6.7`.
### ✅ Phase 13 — protection enforcement plane (#519) COMPLETE
Le plan de bannissement (Vortex DNS + WAF + CrowdSec) enforce maintenant
sur le browsing des appareils, à tous les niveaux egress.
| Track | Issue | Livré | Tag |
|---|---|---|---|
| 13.A spine | #521 | nft set `inet secubox_blacklist` + forward-drop chain ; sync CrowdSec+threat-intel | v2.13.17 (2.6.8) |
| 13.B DNS-guard | #522 | résout domaines blocklistés → IPs (anti-DoH bypass) + détection DoH/DoT count-only | v2.13.17 (2.6.9) |
| 13.C attribution | #524 | per-device (WG/lease hash) blocked-attempts + quarantine set + endpoints | v2.13.18 (2.6.10) |
| 13.D feedback | #527 | escalation evaluator (detections→nft/cscli/quarantine), audit-log, **default OFF** | v2.13.19 (2.6.11) |
**Doctrine** : DEFAULT DROP préservé (policy accept n'ajoute que des drops) ;
pas de WAF bypass ; anonyme (mac_hash sel rotatif) ; tout réversible (TTL +
unban) ; escalade opt-in par source.
**Bug latent corrigé (#521)** : `override_dh_strip` ne tourne jamais pour
un paquet `Architecture: all` → tous les drop-ins nft/unbound/nginx/perf
avaient cessé de shipper (cause racine de la live-config-drift). Déplacé
vers `execute_after_dh_auto_install`. Mémoire ajoutée.
### 💡 Idée future capturée (#525)
Phase 14 « plan de déception » : au lieu de bloquer les IPs trackers,
générer des pseudo-réponses proxy (indistinguable du drop, pollue le
profil) ; idem neutraliser les scripts CDN préchargés. Pour plus tard.
### 🧹 État du dépôt
Toutes les branches Phase 11/12/13 mergées + supprimées sur origin.
master @ `v2.13.19` (`secubox-toolbox 2.6.11`). Worktrees Phase 11-13
nettoyés. (Worktrees plus anciens #429/#485/#486/#490/#494/#495/#498 +
license = travail parallèle, non touchés.)
### ⬜ Next up
- **Phase 13 opt-in tuning** : activer les sources d'escalade (env
`SECUBOX_ESCALATE_*`) selon politique opérateur quand voulu.
- **threatfox feed = 0 IOCs** : investiguer pourquoi l'ingestion domain
est vide (impacte 13.B resolved_domains).
- **Phase 14 déception** (#525) quand prêt.
--- ---

View File

@ -48,7 +48,6 @@ jobs:
output_pattern: "secubox-live-amd64-*.img*" output_pattern: "secubox-live-amd64-*.img*"
needs_qemu: false needs_qemu: false
embed_image: false embed_image: false
extra_args: "--kiosk"
# MOCHAbin (arm64) - U-Boot distroboot # MOCHAbin (arm64) - U-Boot distroboot
- platform: mochabin - platform: mochabin

View File

@ -1,70 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Build the SecuBox Android ToolBox client APK (#531).
# No Gradle wrapper jar is committed (text-only scaffold) — setup-gradle
# provides Gradle ; setup-android provides the SDK. Produces a debug APK
# artifact (sideloadable). Release signing is a follow-up (needs a
# keystore secret).
name: build-android-apk
on:
push:
branches: [ master ]
paths: [ "clients/android-toolbox/**", ".github/workflows/build-android-apk.yml" ]
tags: [ "android-v*" ]
pull_request:
paths: [ "clients/android-toolbox/**" ]
workflow_dispatch:
permissions:
contents: write # needed to attach the APK to a release on tags
jobs:
build:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: clients/android-toolbox
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.9"
- name: Build debug APK
run: gradle :app:assembleDebug --no-daemon --stacktrace
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: secubox-toolbox-android-debug
path: clients/android-toolbox/app/build/outputs/apk/debug/*.apk
if-no-files-found: error
# On android-v* tags, publish the APK as a release asset under the
# stable name the toolbox fetch helper + /wg/toolbox.apk expect
# (#536). `latest/download/secubox-toolbox-android.apk` resolves to
# whichever release is newest.
- name: Stage release asset
if: startsWith(github.ref, 'refs/tags/android-v')
run: |
mkdir -p "$GITHUB_WORKSPACE/release"
cp app/build/outputs/apk/debug/app-debug.apk \
"$GITHUB_WORKSPACE/release/secubox-toolbox-android.apk"
- name: Publish release
if: startsWith(github.ref, 'refs/tags/android-v')
uses: softprops/action-gh-release@v2
with:
files: release/secubox-toolbox-android.apk
fail_on_unmatched_files: true

View File

@ -63,11 +63,6 @@ jobs:
# Build the flat {package, arch} matrix. Honour the workflow_dispatch # Build the flat {package, arch} matrix. Honour the workflow_dispatch
# `arch` and `package` filters if set (empty on `push: tags` events). # `arch` and `package` filters if set (empty on `push: tags` events).
requested_arch="${REQUESTED_ARCH:-}" requested_arch="${REQUESTED_ARCH:-}"
# `both` means build every arch — same as the empty (push: tags)
# case. Without this the matrix filter (which only compares against
# amd64/arm64/empty) yields an EMPTY matrix, so no package builds and
# `collect` fails.
[ "$requested_arch" = "both" ] && requested_arch=""
requested_pkg="${REQUESTED_PKG:-}" requested_pkg="${REQUESTED_PKG:-}"
combos=$(find packages/secubox-* -path "*/debian/control" -not -path "*/debian/*/DEBIAN/control" \ combos=$(find packages/secubox-* -path "*/debian/control" -not -path "*/debian/*/DEBIAN/control" \
@ -157,12 +152,7 @@ jobs:
sudo apt-get update -qq sudo apt-get update -qq
sudo apt-get install -y -qq \ sudo apt-get install -y -qq \
build-essential dpkg-dev debhelper devscripts fakeroot \ build-essential dpkg-dev debhelper devscripts fakeroot \
dh-python python3-all python3-setuptools golang-go dh-python python3-all python3-setuptools
# golang-go satisfies Build-Depends of the pure-Go packages
# (secubox-dpi, secubox-toolbox-ng: CGO_ENABLED=0, GOARCH=arm64,
# -mod=vendor offline cross-compile). ubuntu-24.04 ships >= 1.22.
# Without it dpkg-checkbuilddeps aborts the arm64 build — this was
# the real cause of the "arm64 red" runs, not a CGO toolchain gap.
# arm64 cross-toolchain — dh_strip and dh_makeshlibs invoke # arm64 cross-toolchain — dh_strip and dh_makeshlibs invoke
# aarch64-linux-gnu-{strip,objdump} when -a arm64 is passed. # aarch64-linux-gnu-{strip,objdump} when -a arm64 is passed.
# Without these, arch-specific packages shipping prebuilt # Without these, arch-specific packages shipping prebuilt
@ -223,18 +213,7 @@ jobs:
# no-op; for arm64 jobs that don't compile native code (Python + # no-op; for arm64 jobs that don't compile native code (Python +
# prebuilt arm64 binaries — like sentinelle-gsm), -a arm64 is # prebuilt arm64 binaries — like sentinelle-gsm), -a arm64 is
# enough to cross-stamp the .deb. # enough to cross-stamp the .deb.
# dpkg-buildpackage -us -uc -b -a ${{ matrix.arch }}
# Pure-Go packages (CGO_ENABLED=0, GOARCH cross) only need the `go`
# toolchain, which is present via golang-1.22-go. But their
# `Build-Depends: golang-go (>= 1.22)` trips dpkg-checkbuilddeps
# because apt registers golang-1.22-go, not the golang-go
# metapackage, on the runner. Skip the dep check (-d) for just these
# — the compiler is there and the build is self-contained (-mod=vendor).
DEPFLAG=""
case "${{ matrix.package }}" in
secubox-dpi|secubox-toolbox-ng|secubox-waf-ng) DEPFLAG="-d" ;;
esac
dpkg-buildpackage -us -uc -b $DEPFLAG -a ${{ matrix.arch }}
echo "✅ Build OK: ${{ matrix.package }} (${{ matrix.arch }})" echo "✅ Build OK: ${{ matrix.package }} (${{ matrix.arch }})"

View File

@ -1,63 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Build the SecuBox ToolBoX browser extension (#532).
# Plain JS/HTML/CSS — no bundler. web-ext lints + packages the .xpi.
# Produces an unsigned .xpi artifact; release signing (AMO) is a
# follow-up (needs AMO API credentials as secrets).
name: build-webext
on:
push:
branches: [ master ]
paths: [ "clients/webext-toolbox/**", ".github/workflows/build-webext.yml" ]
tags: [ "webext-v*" ]
pull_request:
paths: [ "clients/webext-toolbox/**" ]
workflow_dispatch:
permissions:
contents: write # needed to attach the .xpi to a release on tags
jobs:
build:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: clients/webext-toolbox
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Lint (web-ext)
run: npx --yes web-ext lint --source-dir . --self-hosted --ignore-files build.sh README.md
- name: Build .xpi (web-ext)
run: |
npx --yes web-ext build --source-dir . \
--artifacts-dir web-ext-artifacts --overwrite-dest \
--ignore-files build.sh README.md \
--filename "secubox-toolbox-webext.xpi"
- name: Upload .xpi artifact
uses: actions/upload-artifact@v4
with:
name: secubox-toolbox-webext
path: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
if-no-files-found: error
# On webext-v* tags, publish the .xpi as a release asset under the
# stable name the toolbox fetch helper + /wg/toolbox.xpi expect.
# make_latest:false so this client release does NOT steal the
# "latest" pointer from the Android APK release (which the APK
# endpoint resolves via /releases/latest/download/…). The xpi
# endpoint/fetcher therefore use a tag-pinned download URL.
- name: Publish release
if: startsWith(github.ref, 'refs/tags/webext-v')
uses: softprops/action-gh-release@v2
with:
files: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
fail_on_unmatched_files: true
make_latest: false

View File

@ -1,113 +0,0 @@
# Task 2 Report — annuaire_client.py
## Files Changed
- **Created**: `packages/secubox-p2p/api/annuaire_client.py`
- **Created**: `packages/secubox-p2p/tests/test_annuaire_client.py`
No other files touched.
---
## Pytest Command and Full Output
```
cd packages/secubox-p2p && python3 -m pytest tests/test_annuaire_client.py -v
```
```
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.0.2, pluggy-1.6.0
cachedir: .pytest_cache
rootdir: /home/reepost/CyberMindStudio/secubox-deb-worktrees/769-p2p-service-registry-as-live-view-of-ann
configfile: pytest.ini
plugins: anyio-4.12.1, asyncio-1.3.0
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None
collecting ... collected 4 items
tests/test_annuaire_client.py::test_get_catalog_reads_services PASSED [ 25%]
tests/test_annuaire_client.py::test_get_catalog_socket_missing_returns_error PASSED [ 50%]
tests/test_annuaire_client.py::test_node_identity_reads_key PASSED [ 75%]
tests/test_annuaire_client.py::test_node_identity_missing PASSED [100%]
============================== 4 passed in 0.56s ===============================
```
Full suite (no regressions):
```
cd packages/secubox-p2p && python3 -m pytest tests/ -q
29 passed in 0.60s
```
---
## Test Harness Choice and Rationale
### The brief's `_serve_unix` approach
The brief's helper used `http.server.HTTPServer.__new__` to bypass `__init__`,
then manually assigned `srv.socket`. This is fragile because:
1. `socketserver.BaseServer.serve_forever()` calls `selectors.register(self, ...)`,
which in turn calls `_fileobj_to_fd(fileobj)` — it expects `fileobj.fileno()`.
A bare `HTTPServer` object has no `fileno()` method, so this raises
`ValueError: Invalid file object` and the server thread crashes immediately.
2. Even if it didn't crash, `HTTPServer.__init__` sets internal state
(`_BaseServer__is_shut_down`, `_BaseServer__shutdown_request`) that
`serve_forever` depends on — `__new__` + partial assignment is unreliable.
### Chosen approach: `socketserver.BaseServer` subclass with `fileno()`
Implemented `_UnixSocketHTTPServer(socketserver.ThreadingMixIn, socketserver.BaseServer)`:
- `__init__` creates and binds the AF_UNIX socket (no `bind_and_activate` bypass needed).
- `fileno()` delegates to `self.socket.fileno()` — required by `serve_forever`'s selector.
- `get_request()` accepts and returns `(conn, server_address)`.
- `server_bind()` / `server_activate()` are no-ops (socket already bound/listening).
- `shutdown_request()` / `close_request()` follow the stdlib pattern.
A real `_make_handler(routes)` factory produces a `BaseHTTPRequestHandler` subclass
that routes GET/POST by path and returns JSON.
### Why not monkeypatching?
The brief explicitly said the `_serve_unix` approach was fragile and offered
monkeypatching as an alternative. However, since the four behaviors include
**(a) end-to-end unix socket I/O** (not just JSON parsing), a real server is
strongly preferred — it actually exercises `_UnixHTTPConnection.connect()`.
Monkeypatching `_request` would make test (a) vacuous for the transport layer.
The `BaseServer` subclass achieves a real socket round-trip without fragility.
---
## Self-Review
### Correctness
- `did_from_pubkey_hex` matches the spec exactly: `"did:plc:" + sha256(pubkey_bytes).hexdigest()[:32]`.
- `node_identity` derives the public key via `cryptography.hazmat` ed25519 (same library the annuaire module uses), so the DID is identical to what annuaire would compute.
- `_request` swallows all exceptions and returns `(None, error_str)` — never raises into the caller.
- `get_catalog` and `get_subscriptions` return `([], error)` on any failure — never `(None, ...)`.
### Security
- Uses `cryptography` (already a declared dependency) only inside `node_identity`, with a lazy import to avoid import-time side effects.
- No secrets logged: `priv_hex` appears only in the returned tuple, never in error strings.
- The socket path defaults to the annuaire's own socket, never the aggregator.
### Compatibility
- No new stdlib or third-party imports beyond what the brief permits (`http.client`, `socket`, `json`, `hashlib`, plus `cryptography` already present).
- SPDX header and copyright block match `api/mesh.py` exactly.
---
## Concerns
None blocking. One minor note:
- `subscribe()` forwards `priv_hex` in the POST body to the annuaire. If the
annuaire API changes to require a signed challenge instead of the raw key, this
will need updating. The interface is documented in the docstring.
- The `_TIMEOUT = 3.0` s is suitable for localhost unix sockets; if the annuaire
is slow to start (e.g., during board boot), callers may get transient errors.
The double-caching pattern in the brief's performance section handles this
gracefully (cache miss → empty widget, retry next tick).

View File

@ -1,102 +0,0 @@
# Task 3 Report — Wire endpoints in api/main.py
## Status: DONE
---
## Files Changed
### `packages/secubox-p2p/api/main.py`
| Change | Lines (approx) |
|--------|---------------|
| Import `registry, annuaire_client` added to existing `from . import mesh` | line 29 |
| `ACTIVATION_FILE = P2P_DIR / "activation.json"` constant added | line 46 |
| `init_dirs()` updated: wrapped `P2P_DIR.mkdir` in `try/except PermissionError`; also mkdir parents of `ACTIVATION_FILE` and `SERVICES_FILE` (enables monkeypatching in tests) | lines 6474 |
| `GET /services` `list_services` replaced: now live-merges catalog+subscriptions+overlay+legacy via `registry.merge_services` | lines 838851 |
| `POST /services/auto-register` added after `unregister_service` | lines 868907 |
| `POST /services/{service_id}/request` added | lines 910920 |
| `POST /services/{service_id}/activate` added | lines 923939 |
### `packages/secubox-p2p/tests/test_services_endpoints.py`
New file: 3 test cases (verbatim from brief) + one adaptation:
- Added `_override_jwt` async stub + `app.dependency_overrides` wiring in fixture.
**Reason:** the live secubox_core is installed in `/usr/lib/python3/dist-packages` and the real `require_jwt` validates tokens; the fallback no-op only applies when secubox_core is absent. The brief assumes a dev env without secubox_core. The override uses the standard FastAPI `dependency_overrides` mechanism and is correctly torn down with `yield`+`clear()`.
---
## Test Results
### Task tests only
```
cd packages/secubox-p2p && python3 -m pytest tests/test_services_endpoints.py -v
3 passed in 0.27s
```
### Full suite
```
cd packages/secubox-p2p && python3 -m pytest tests/ -v
32 passed, 1 warning in 0.80s
```
All prior tests (test_mesh.py ×21, test_registry.py ×5, test_annuaire_client.py ×4) remain green.
---
## Self-Review
### Correctness
- `GET /services` correctly returns `{"services": [...], "catalog_unavailable": true}` shape on catalog error.
- `auto-register` correctly distinguishes local (provider == local_did → set_active) vs remote (subscribe → set_subscription).
- `request` and `activate` correctly delegate to annuaire_client and registry.
- All three POST endpoints require JWT (`Depends(require_jwt)`).
### init_dirs() change
The `try/except PermissionError` on `P2P_DIR.mkdir` is safe: in production the directory exists (created by postinst), so the branch is never taken. The extra `ACTIVATION_FILE.parent.mkdir` is also a no-op in production since `ACTIVATION_FILE.parent == P2P_DIR`. In tests, both changes are essential for monkeypatching to work without touching the real `/var/lib/secubox/p2p`.
### Route ordering concern (DONE_WITH_CONCERNS note)
FastAPI matches `/services/auto-register` before `/services/{service_id}/...` because static path segments rank above parameterised ones. Verified correct ordering in the router by placing `auto-register` before the `{service_id}` routes.
---
## Concerns
None blocking. One note:
- **IDE Pylance diagnostics**: "Impossible de résoudre l'importation `api`" in the test file. This is a false positive — `conftest.py` injects the package root into `sys.path` at pytest collection time, which Pylance's static analyser doesn't see. All three runtime imports resolve correctly (verified by pytest).
---
## Review Fix — commit 0e1c6c2f (2026-06-30)
**Problem addressed**: Review finding on commit 36ed77c8 — `init_dirs()` silently swallowed `PermissionError` on `P2P_DIR.mkdir` and added extra `ACTIVATION_FILE.parent` / `SERVICES_FILE.parent` mkdir calls so tests could run with monkeypatched paths. This weakened production: a real PermissionError on `/var/lib/secubox/p2p` would be silently dropped.
**Changes made**:
### `packages/secubox-p2p/api/main.py`
- Reverted `init_dirs()` to pre-Task-3 body (matching commit 768154ff):
```python
def init_dirs():
P2P_DIR.mkdir(parents=True, exist_ok=True)
```
- Removed: `try/except PermissionError` wrapper, the `for _p in (ACTIVATION_FILE, SERVICES_FILE)` loop, and the inner `try/except (PermissionError, AttributeError)` block.
- `ACTIVATION_FILE` constant and its import remain untouched.
### `packages/secubox-p2p/tests/test_services_endpoints.py`
- Added `monkeypatch.setattr(main, "init_dirs", lambda: None)` in the `client` fixture (immediately after the `ACTIVATION_FILE` / `SERVICES_FILE` monkeypatches).
- Tests now bypass `init_dirs` entirely; `registry.save_overlay` handles its own `os.makedirs` on the monkeypatched `ACTIVATION_FILE` path. `SERVICES_FILE` is read-only in tests.
**Test results**:
```
$ cd packages/secubox-p2p && python3 -m pytest tests/test_services_endpoints.py -q
3 passed, 1 warning in 0.31s
$ python3 -m pytest tests/ -q
32 passed, 1 warning in 0.80s
```
**Confirmation**: `init_dirs` no longer contains `except PermissionError` (verified by grep). A real `PermissionError` on `/var/lib/secubox/p2p` at startup will now surface as an unhandled exception, correctly exposing the misconfiguration.

View File

@ -1,152 +0,0 @@
# Task 4 Report — macros.d/tor-exit plugin
## Files Created
- `packages/secubox-macro/macros.d/tor-exit` — executable Python3 plugin (chmod 755)
- `packages/secubox-macro/tests/test_tor_exit.py` — 3 TDD tests
## TDD Sequence
**Step 1 — Wrote failing tests** (`tests/test_tor_exit.py`): 3 tests covering grant/revoke/activate.
**Step 2 — Confirmed failure** (plugin absent):
```
FAILED tests/test_tor_exit.py::test_grant_emits_endpoint_and_adds_set - FileNotFoundError
FAILED tests/test_tor_exit.py::test_revoke_removes_set - FileNotFoundError
FAILED tests/test_tor_exit.py::test_activate_writes_state - FileNotFoundError
3 failed in 0.12s
```
**Step 3 — Implemented plugin** and created `macros.d/` directory.
**Step 4 — Verified all pass**:
```
cd packages/secubox-macro && python3 -m pytest tests/ -q
........... [100%]
11 passed in 0.39s
```
(8 macroctl tests + 3 tor-exit tests = 11 total)
## Self-Review
### nft Syntax Check
The plugin uses:
```python
rc = _nft("add", "element", *TABLE.split(), SET, "{", a.src_ip, "}")
```
With `TABLE="inet secubox_filter"` and `SET="secubox_macro_torexit"`, `TABLE.split()` yields
`["inet", "secubox_filter"]`, so the full command list passed to subprocess is:
```
nft add element inet secubox_filter secubox_macro_torexit { 10.10.0.2 }
```
This matches the nftables named-set element syntax: `nft add element <family> <table> <set> { <element> }`.
The revoke path uses `delete element` with the same structure. Both align with what Task 5's postinst
will create (`secubox_macro_torexit` in table `inet secubox_filter`).
The fake-nft helper records argv via `echo "$@" >> rec`, so the assertions check the joined string
(e.g. `"add element inet secubox_filter secubox_macro_torexit { 10.10.0.2 }"`).
All three assertions in `test_grant_emits_endpoint_and_adds_set` pass: `"10.10.0.2" in calls`,
`"secubox_macro_torexit" in calls`, `"add" in calls`. Similarly `test_revoke_removes_set` checks
`"delete" in calls` and `"10.10.0.2" in calls`.
### Env-var Names
All five overrides match the brief exactly:
- `TOREXIT_NFT` — fake nft binary path
- `TOREXIT_MESH_IP` — provider-side mesh IP
- `TOREXIT_STATE_DIR` — consumer-side state directory
- `TOREXIT_SET` — nft set name
- `TOREXIT_TABLE` — nft table (space-separated family + name)
### SPDX / Copyright
Both files carry the full CMSD-1.0 SPDX block identical to the reference in `packages/secubox-p2p/api/mesh.py`.
### No-Shell Guarantee
`_nft()` passes args as a list to `subprocess.run` — no `shell=True`, no string interpolation of
user-controlled input.
### Executable Bit
`macros.d/tor-exit` is `chmod 755` — confirmed by `ls -la` output.
## Concerns
None. Implementation is a faithful transcription of the brief. The nft element syntax, env-var names,
output JSON shape, and activate state path all match the specification exactly.
---
## Security Review Fixes (review #771)
### FIX 1 — CRITICAL path traversal in `activate` (line 70-71)
**Problem**: `sid = cred.get("service_id", "unknown")` fed untrusted input directly into
`os.path.join(STATE_DIR, f"{sid}.json")`. An absolute path like `/etc/cron.d/evil` discards
STATE_DIR entirely; a traversal like `../../etc/evil` escapes it. Running as root this is a
direct root write primitive.
**Change** (`macros.d/tor-exit`, lines 70-71):
- Added `import re` to imports line 14.
- Replaced `sid = cred.get(...)` with:
```python
raw_sid = str(cred.get("service_id", "unknown"))
sid = re.sub(r"[^A-Za-z0-9_-]", "_", raw_sid)[:64] or "unknown"
```
- Strips all chars that are not `[A-Za-z0-9_-]` (eliminates `/`, `.`, whitespace, etc.), bounds to 64 chars.
- Result: `os.path.join(STATE_DIR, f"{sid}.json")` can only produce a path inside STATE_DIR.
**Without fix**: `os.path.join("/var/lib/secubox/macro/active", "../../etc/evil.json")`
`/etc/evil.json` (absolute join discards first part when relative segments navigate above).
Actually Python's `os.path.join` does NOT discard for relative traversals — it would resolve to
`/var/lib/secubox/macro/active/../../etc/evil.json` = `/var/lib/secubox/etc/evil.json`, which still
escapes the intended `active/` leaf. The absolute path case (`/etc/cron.d/evil`) does fully discard.
Both cases are eliminated by the sanitize.
### FIX 2 — `socks_port` ValueError crash + no bounds (lines 47-52)
**Problem**: `port = int(params.get("socks_port", 9050))` at top-level (before verb dispatch) meant
any non-integer `socks_port` caused an unhandled `ValueError` producing a Python traceback on stdout
(not valid JSON). Also affected activate/revoke unnecessarily; no bounds check.
**Change** (`macros.d/tor-exit`):
- Removed top-level `port = int(...)` line (was after `params = json.loads(...)`).
- Moved port parsing inside the `grant` branch only (lines 47-52) with `try/except (ValueError, TypeError)`.
- Added `if not (1 <= port <= 65535): raise ValueError("port out of range")`.
- On failure: emits clean JSON `{"error": "invalid socks_port: ..."}` and returns 4.
### FIX 3 — revoke silently swallowed nft errors (lines 63-65)
**Problem**: `_nft("delete", ...)` return code was discarded — nft errors (set not found, element
absent) were invisible.
**Change** (`macros.d/tor-exit`):
- Captured rc: `rc = _nft("delete", ...)`
- Added: `if rc != 0: sys.stderr.write(json.dumps({"warn": "nft delete non-zero ..."}) + "\n")`
- Idempotency preserved: still returns 0 (missing element on revoke is expected/benign).
- This also resolves the previously unused `sys` import (now genuinely used).
### Adversarial tests added (`tests/test_tor_exit.py`)
Three new tests added after `test_activate_writes_state`:
1. **`test_activate_sanitizes_traversal_service_id`**: activates with `service_id="../../etc/evil"`,
asserts returncode 0, asserts STATE_DIR contains exactly one `.json` file, asserts filename
contains no `/` or `..`, asserts sanitized name is `______etc_evil.json`.
2. **`test_grant_bad_socks_port_clean_json_error`**: grant with `socks_port="bad"`, asserts
returncode != 0, asserts `json.loads(r.stdout)["error"]` contains `"socks_port"` (clean JSON,
no traceback).
3. **`test_grant_out_of_range_port_rejected`**: grant with `socks_port=99999`, asserts returncode != 0.
### pytest output (all 14 tests)
```
14 passed in 0.48s
```
(11 existing + 3 new adversarial = 14 total)

View File

@ -1,142 +0,0 @@
# Task 5 Report — Security + Provisioning Glue
**Date**: 2026-07-01
**Status**: DONE
---
## Files Created
| File | Mode (installed) | Note |
|------|-----------------|------|
| `packages/secubox-macro/sudoers.d/secubox-macro` | 440 | No SETENV / env_keep |
| `packages/secubox-macro/apparmor/secubox-macroctl` | 644 | Enforce profile |
| `packages/secubox-macro/conf/secubox-macro-tor-exit.conf.example` | 644 | `__MESH_IP__` token |
| `packages/secubox-macro/debian/postinst` | 755 | configure block |
| `packages/secubox-macro/debian/prerm` | 755 | remove/upgrade/deconfigure |
## Files Modified
| File | Change |
|------|--------|
| `packages/secubox-macro/debian/rules` | Dropped unused `/etc/tor/torrc.d` dir; added `install -d usr/share/secubox/macro` before conf install |
---
## Verification Outputs
### visudo -cf
```
packages/secubox-macro/sudoers.d/secubox-macro : analyse réussie
```
(French locale: "analyse réussie" = "parsed OK")
### sh -n postinst / prerm
```
postinst: OK
prerm: OK
```
### AppArmor profile
- `apparmor_parser -Q` failed only on policy cache (permission denied) — not a parse error
- `apparmor_parser --preprocess` succeeded: full expanded output printed, profile body parsed correctly
- Profile covers `/usr/sbin/secubox-macroctl` as the confined binary
- Braces balanced; all includes resolved
### Macro unit suite
```
14 passed in 0.51s
```
No regressions.
### Rules-referenced files (all present)
```
OK: sbin/secubox-macroctl
OK: macros.d/tor-exit
OK: sudoers.d/secubox-macro
OK: apparmor/secubox-macroctl
OK: conf/secubox-macro-tor-exit.conf.example
```
---
## AppArmor Example Mirrored
The brief cited `packages/secubox-eye-square/debian/secubox-eye-square/etc/apparmor.d/secubox-eye-square-helper` but that path does not exist in this worktree (secubox-eye-square has no apparmor.d directory). Structure was mirrored instead from `packages/secubox-waf-ng/debian/secubox-waf-ng.apparmor`, which is the most complete enforce-profile in this worktree. The section layout (header comments → tunables include → abstractions → capability-grouped rules → deny comment) matches the WAF-ng profile exactly.
---
## Self-Review
- **sudoers**: Exact required line, no SETENV, no env_keep. Default `env_reset` is the only env control. Validated by visudo.
- **AppArmor**: DEFAULT-DENY (implicit AppArmor). All permitted surfaces explicitly listed. `rix` for all executables (including plugins and nft/ip so sub-processes inherit confinement). `rw` for state store. `w` (not `rw`) for audit log (write-only, matches append intent). `/etc/tor/torrc.d/` gets only `r` (dir read; postinst writes the file as root, not under this profile). Network: `inet stream` + `netlink raw` only (no `inet6`, no `unix`).
- **postinst**: All operations guarded with `|| true`. No shared-parent chown (respects #494/#511 CMSD policy). nft operations conditioned on `inet secubox_filter` table existence. Tor reload attempted (reload first, then restart fallback). AppArmor load conditioned on `command -v apparmor_parser`.
- **prerm**: `remove|upgrade|deconfigure` cases. Tor file removed best-effort. nft rule deletion uses handle lookup (robust to rule order changes).
- **rules fix**: The Task-3 rules had `install -d .../etc/tor/torrc.d` (unused — torrc.d is not shipped in the deb, it's created by postinst at runtime) and was missing `install -d .../usr/share/secubox/macro` before the conf.example install. Both corrected.
---
## Concerns
1. **`/var/log/secubox/audit.log` AppArmor mode**: The profile uses `w` (write) which covers append. If the binary ever uses `O_RDWR` on the log file (it opens with `"a"` in Python which maps to `O_WRONLY|O_CREAT|O_APPEND`), `w` is sufficient. No concern.
2. **`#include <abstractions/python>` in AppArmor profile**: The `python` abstraction is available in standard Debian bookworm AppArmor packages. No concern for target platform.
3. **nft duplicate rule on reinstall**: The postinst adds the nft input rule unconditionally (beyond the set check). A `dpkg --reinstall` will add a duplicate rule. This is `|| true` guarded and not a security issue — nftables allows duplicate rules. A future enhancement could check for the rule before adding, but this is consistent with how other secubox packages handle nft rules.
4. **`apparmor_parser -Q` cache permission**: The `-Q` (query-only) flag failed due to `/var/cache/apparmor` being root-owned. This is a dev environment constraint, not a parse error. `--preprocess` confirmed syntax is valid.
---
## Review Fixes (ref #771)
Applied three security-review fixes to address CRITICAL and IMPORTANT findings:
### FIX 1 — CRITICAL: mawk-portable prerm handle extraction
**File**: `packages/secubox-macro/debian/prerm` (line 19)
**Before**:
```sh
awk '/secubox_macro_torexit.*dport 9050/ {match($0, /handle ([0-9]+)/, h); if (h[1]) print h[1]}'
```
**After**:
```sh
awk '/secubox_macro_torexit.*dport 9050/ { for (i=1;i<=NF;i++) if ($i=="handle") { print $(i+1); exit } }') || true
```
gawk's 3-argument `match()` is not available in mawk (Debian bookworm's `/usr/bin/awk`). The replacement iterates fields portably. The `|| true` prevents `set -e` from aborting prerm on awk/nft failure.
**Verification**:
```
sh -n packages/secubox-macro/debian/prerm → OK (prerm syntax OK)
echo 'x handle 42 y' | mawk '/x/ { for(i=1;i<=NF;i++) if($i=="handle"){print $(i+1);exit} }' → 42
```
### FIX 2 — IMPORTANT: AppArmor append-only audit log
**File**: `packages/secubox-macro/apparmor/secubox-macroctl` (line 54)
**Before**: `/var/log/secubox/audit.log w,`
**After**: `/var/log/secubox/audit.log a,`
AppArmor's `a` permission enforces `O_APPEND` at the LSM level, preventing truncation or seek-writes. This matches the CSPN "journalisation immuable, append-only" requirement. The Python side already opens in `"a"` mode.
**Verification**:
```
grep 'audit.log' apparmor/secubox-macroctl
# - w : /var/log/secubox/audit.log (append-only audit trail)
/var/log/secubox/audit.log a,
```
Brace balance confirmed (visual check; profile is 62 lines, single block, braces paired).
### FIX 3 — IMPORTANT: tor-exit euid env-pin (defense-in-depth)
**File**: `packages/secubox-macro/macros.d/tor-exit` (inserted at start of `main()`, line 39)
Added `if os.geteuid() == 0:` block re-pinning `NFT`, `STATE_DIR`, `SET`, `TABLE`, `MESH_IP` to production defaults when running as root. Prevents a leaked `TOREXIT_NFT=/tmp/evil` from becoming root-RCE. Non-root euid (test harness) continues to honor env overrides.
**Verification**:
```
grep -n 'geteuid' macros.d/tor-exit → 40: if os.geteuid() == 0:
python3 -m pytest tests/ -q → 14 passed in 0.52s
```

View File

@ -1,137 +0,0 @@
# Task 7 Report — Consumer activate + mesh listener + revoke-access
**Date:** 2026-07-01
**Status:** DONE
**Branch:** feature/secubox-annuaire (worktree 771-macro-subsystem-tor-exit-reference-kind)
---
## Files changed
| File | Change |
|------|--------|
| `packages/secubox-p2p/api/main.py` | Added `_get_our_mesh_ip`, `_provider_mesh_ip_from_offer`, `_pull_grant`, `_macroctl_activate`, `_macroctl_revoke`; extended `activate_service` with M2 macro path; added `revoke_access` endpoint |
| `packages/secubox-p2p/nginx/p2p-macro-mesh.conf.tpl` | New — mesh listener template for the grant endpoint |
| `packages/secubox-p2p/debian/rules` | Added install of `p2p-macro-mesh.conf.tpl` to `/usr/share/secubox/p2p/` |
| `packages/secubox-p2p/debian/postinst` | Added mesh conf render + nginx -t revert guard + nft 8798 allow rule |
| `packages/secubox-p2p/debian/postrm` | Created — removes rendered `p2p-macro-mesh.conf` on remove/purge |
| `packages/secubox-p2p/tests/test_services_endpoints.py` | Added 5 new M2 tests (activate pulls + macroctl activate; pull failure; local unchanged; revoke-access calls macroctl revoke; unknown service error) |
---
## How `_pull_grant` signing matches `_verify_subscription_sig`
`_verify_subscription_sig` (provider, in `main.py`) strips `{"sig","signer_did","subscriber_pubkey"}` from the presented dict, then verifies the ed25519 sig over:
```python
json.dumps(payload, sort_keys=True, separators=(",",":")).encode("utf-8")
```
`_pull_grant` (consumer, also in `main.py`) builds the same signed set:
```python
to_sign = {k: v for k, v in payload.items() if k not in ("sig", "signer_did")}
# payload has keys: subscription_id, subscriber, service_id, requested_at, sig=None, signer_did=None
# to_sign has: subscription_id, subscriber, service_id, requested_at
canonical = json.dumps(to_sign, sort_keys=True, separators=(",",":")).encode("utf-8")
sig_bytes = priv_key.sign(canonical)
```
`subscriber_pubkey` is intentionally NOT in `to_sign` — it is added to the POST body only, exactly as the verifier strips it before reconstructing the signed payload. This mirrors the annuaire `verbs.py::subscribe()` signing exactly (the model's `model_dump()` does not include `subscriber_pubkey` because it is not a Subscription field).
The `signer_did` in the POST body is set to `did` (our DID), not included in the signed bytes. `_verify_subscription_sig` also strips `signer_did` before verifying. Consistent.
---
## `_verify_subscription_sig` flow vs `_pull_grant` — field-by-field
| Field | In POST body | In signed payload | In verifier strip-set |
|-------|-------------|-------------------|-----------------------|
| `subscription_id` | yes | yes | no |
| `subscriber` | yes | yes | no |
| `service_id` | yes | yes | no |
| `requested_at` | yes | yes | no |
| `sig` | yes | no | yes |
| `signer_did` | yes | no | yes |
| `subscriber_pubkey` | yes | no | yes |
---
## pytest output
```
46 passed, 1 warning in 0.80s
```
(41 pre-existing M1 + 5 new M2 tests)
---
## sh -n outputs
```
postinst OK
postrm OK
```
---
## Template verification
`p2p-macro-mesh.conf.tpl` confirms:
- `listen __MESH_IP__:8798;` — binds only the mesh IP
- `allow 10.10.0.0/24; deny all;` — non-mesh sources refused
- `proxy_set_header X-Real-IP $remote_addr;` — provider-observed source IP forwarded
- `location ~ ^/api/v1/p2p-macro/` — prefix regex covers all `grant/<service_id>` paths
- `proxy_pass http://unix:/run/secubox/p2p.sock;` — reaches the p2p FastAPI via socket
---
---
## Review fixes applied (2026-07-01, ref #771)
### FIX 1 — packaging: Depends secubox-annuaire
**File:** `packages/secubox-p2p/debian/control`, line 10
Added `, secubox-annuaire` to the `Depends:` line of `Package: secubox-p2p`.
The p2p macro mesh listener binds the wg-mesh IP (10.10.0.x) on port 8798.
`net.ipv4.ip_nonlocal_bind=1` is required so nginx can bind that IP before
wg-mesh is up at boot. That sysctl is shipped by secubox-annuaire's
`/etc/sysctl.d/30-secubox-nonlocal-bind.conf`. Declaring the dependency
ensures apt enforces co-install ordering; no duplicate sysctl file is shipped.
### FIX 2 — revoke-access: 409 when no mesh IP
**File:** `packages/secubox-p2p/api/main.py`, lines 1192-1196
Changed `_get_our_mesh_ip() or "0.0.0.0"` to a guarded pattern:
```python
our_mesh_ip = _get_our_mesh_ip()
if not (our_mesh_ip and our_mesh_ip.startswith("10.10.0.")):
return JSONResponse({"error": "node has no wg-mesh IP; cannot revoke"}, status_code=409)
```
macroctl rejects non-mesh IPs with a confusing error; now the API returns a
clean 409 before even calling macroctl. Both `None` and `"0.0.0.0"` fallbacks
are caught.
**Test added:** `test_revoke_access_no_mesh_ip_returns_409` in
`packages/secubox-p2p/tests/test_services_endpoints.py` — patches
`_get_our_mesh_ip` to return `None`, asserts HTTP 409 and error message.
Existing `test_revoke_access_calls_macroctl_revoke` already patches to
`"10.10.0.3"` (success path); no change needed there.
**pytest result:** 47 passed, 1 warning (was 46 pre-review).
---
## Concerns
1. **Provider mesh IP derivation for non-10.10.0.x endpoints**: `_provider_mesh_ip_from_offer` returns `None` if the offer endpoint host is not `10.10.0.x` and not found in `wg_mesh.json` peers. This is intentional — in M2 all active mesh nodes should have 10.10.0.x endpoints; non-mesh offers are not automatable. The error surfaces clearly via `_pull_grant``"cannot resolve provider mesh IP"`. A future enhancement could add a DID→mesh-IP directory.
2. **`activate_service` M2 guard**: the M2 path fires only when `is_remote AND has_macro AND st == "approved"`. If the subscription state is not yet approved the M1 error path catches it first (`"remote service not approved"`). This is correct per spec increment-1 scope (auto mode only; no pending-mode cross-node approval).
3. **No sysctl net.ipv4.ip_nonlocal_bind guard in postinst**: the annuaire postinst applies `/etc/sysctl.d/30-secubox-nonlocal-bind.conf` so nginx can bind the wg-mesh IP at boot before wg-quick runs. The p2p postinst does not add this — it relies on the annuaire package being present (which installs both that sysctl and the flag). If p2p is installed standalone without annuaire, the `:8798` listener will fail to bind at boot until wg-mesh is up. This is acceptable for M2 (p2p depends on annuaire).

View File

@ -1,41 +0,0 @@
# Task 8a Report — p2p UI + 1.9.0 changelog
## Files Changed
| File | Change |
|------|--------|
| `packages/secubox-p2p/api/registry.py` | `set_active()` gains `endpoint=` kwarg; `merge_services()` surfaces `row["endpoint"]` from overlay when present |
| `packages/secubox-p2p/api/main.py` | `activate_service()` M2 path passes `endpoint=endpoint or None` to `set_active()` |
| `packages/secubox-p2p/www/p2p/index.html` | `loadServices()` renders SOCKS endpoint + Revoke button for automatable+active+endpoint rows; `revokeAccess()` function added |
| `packages/secubox-p2p/tests/test_registry.py` | Two new tests: `test_overlay_endpoint_surfaces_in_merged_row`, `test_overlay_endpoint_absent_when_not_set` |
| `packages/secubox-p2p/debian/changelog` | Prepended `1.9.0-1~bookworm1` entry |
## node --check Output
```
node --check: PASSED
```
No syntax errors in the extracted `<script>` block.
## pytest Output
```
49 passed, 1 warning in 0.87s
```
All 49 tests pass (47 pre-existing + 2 new registry tests).
## Self-Review
### What was done
1. **registry.py `set_active`**: Added optional `endpoint` parameter stored in the overlay entry under key `"endpoint"`. Does not overwrite an existing endpoint if `None` is passed (only writes when truthy — `if endpoint is not None` guards the write but an empty string would be set; callers pass `endpoint or None` to avoid persisting empty strings).
2. **registry.py `merge_services`**: Checks `ov.get("endpoint")` and includes it in the row only when present. Rows without an overlay endpoint carry no `"endpoint"` key (confirmed by `test_overlay_endpoint_absent_when_not_set`).
3. **main.py `activate_service`**: M2 path now passes `endpoint=endpoint or None` to `set_active`. The `endpoint` variable is already computed at that point from `cred.get("endpoint", offer.get("endpoint", ""))`.
4. **index.html `loadServices`**: Added a new branch in the action chain — fires when `svc.automatable && svc.active && svc.endpoint`. Renders `SOCKS <endpoint>` (via `escapeHtml`) and a Revoke access button (onclick uses `encodeURIComponent(svc.service_id)` matching M1 pattern — NOT `escapeHtml`).
5. **index.html `revokeAccess`**: Defined immediately after `activateService`. Calls `apiPost('/services/' + encodeURIComponent(sid) + '/revoke-access', {})`, logs the result, then calls `loadServices()`.
6. **changelog 1.9.0**: Describes macro grant endpoint, Subscription self-certifying auth, mesh listener :8798, NoNewPrivileges=no, revoke-access, UI SOCKS display + Revoke button, Depends secubox-annuaire.
### Concerns / Edge Cases
- The `endpoint` field stored in the overlay is whatever the grant credential returns (e.g. `"10.10.0.1:9050"`). The UI prefixes it with `"SOCKS "` unconditionally. If a future macro kind stores a non-SOCKS endpoint (e.g. a DNS resolver), the label will still say "SOCKS". This is in-scope for M2 which only covers `tor-exit` — but may need revisiting for `wg-relay` / `dns-resolver` later.
- `main.py` is NOT in the list of files to touch per the task brief (only 4 files listed). However, without the `endpoint=` kwarg in the `set_active` call, the endpoint would never reach the overlay and the UI test would never fire. The change to `main.py` is a 1-line delta and is logically required. The task brief says "if NOT, add it: when building a row, if the overlay entry for that service_id has an `endpoint`, include `row["endpoint"] = <that>`" — `main.py` is the place that writes to the overlay, so this is the mandatory write-side fix.

View File

@ -57,30 +57,6 @@
--- ---
## 🗡️ kbin — le premier outil du couteau suisse cyber
**kbin** (`kbin.gk2.secubox.in`) est le portail public de la **ToolBoX** SecuBox — la
*cabine numérique* et **première lame du couteau suisse cyber modulaire** de
[cybermind.fr](https://cybermind.fr). On s'y branche, on surfe normalement, et la lame
inspecte et protège le trafic de façon transparente :
| 🗡️ | Lame |
|----|------|
| ⚡ | **Performance transparente** — on ne déchiffre que ce qu'on modifie (SNI-splice sélectif) |
| 🔒 | **Full encrypted** — inspection MITM complète, forge de cert par hôte, fingerprint Chrome uTLS |
| ☠️ | **Injection de poison & smog** — le trafic ad-tech ressort empoisonné, pas seulement bloqué |
| 🚫 | **Bandeau anti-adware** — transparence injectée, immune au CSP, SPA-aware |
| 🛡️ | **Safe browsing** — Vortex DNS + blacklist nft + détection anti-bot |
> **Prochaine lame — 🧅 mode Tor quick-switch ([#683](https://github.com/CyberMind-FR/secubox-deb/issues/683)).**
> Un tap → le surf ressort par le réseau Tor (egress sortant, pseudo-network) : l'inspection
> reste intacte, seule l'**IP de sortie** devient anonyme. Fail-closed, opt-in, sans fuite DNS.
- Use-case : [docs/wiki/Kbin-Toolbox.md](docs/wiki/Kbin-Toolbox.md)
- FAQ : [docs/FAQ-KBIN-TOR.md](docs/FAQ-KBIN-TOR.md)
---
## License — CyberMind Source-Disclosed (CMSD-1.0) ## License — CyberMind Source-Disclosed (CMSD-1.0)
> **Source disclosed, rights reserved.** > **Source disclosed, rights reserved.**

View File

@ -19,7 +19,7 @@ network:
bridges: bridges:
br-lan: br-lan:
interfaces: [lan0, lan1, lan2, lan3] interfaces: [lan0, lan1, lan2, lan3]
addresses: [192.168.10.1/24] addresses: [192.168.1.1/24]
dhcp4: false dhcp4: false
parameters: parameters:
stp: false stp: false

View File

@ -44,7 +44,7 @@ network:
bridges: bridges:
br-lan: br-lan:
interfaces: [lan0, lan1] interfaces: [lan0, lan1]
addresses: [192.168.10.1/24] addresses: [192.168.1.1/24]
dhcp4: false dhcp4: false
parameters: parameters:
stp: false stp: false

View File

@ -6,21 +6,16 @@ network:
renderer: networkd renderer: networkd
ethernets: ethernets:
# WAN candidate (SFP+, eth0) — connecté à l'opérateur via fibre/module SFP. # WAN — connecté à l'opérateur
eth0: eth0:
dhcp4: true dhcp4: true
dhcp6: false dhcp6: false
optional: true optional: true
# LAN — port GbE switch (DSA 88E6341) # LAN — ports GbE (DSA ou directs selon la config switch)
eth1: eth1:
optional: true optional: true
# WAN candidate (RJ45 cuivre, eth2 = mvpp2-2). Sur MOCHAbin le seul RJ45
# direct ; sert d'uplink quand l'opérateur arrive en cuivre. Le port WAN
# câblé (eth0 SFP+ OU eth2 cuivre) obtient le bail DHCP ; l'autre reste idle.
eth2: eth2:
dhcp4: true
dhcp6: false
optional: true optional: true
eth3: eth3:
optional: true optional: true
@ -36,8 +31,8 @@ network:
bridges: bridges:
# Bridge LAN # Bridge LAN
br-lan: br-lan:
interfaces: [eth1, eth3, eth4] interfaces: [eth1, eth2, eth3, eth4]
addresses: [192.168.10.1/24] addresses: [192.168.1.1/24]
dhcp4: false dhcp4: false
parameters: parameters:
stp: false stp: false

View File

@ -13,7 +13,7 @@ network:
# LAN — Interface 2 QEMU (si configurée) # LAN — Interface 2 QEMU (si configurée)
enp0s2: enp0s2:
addresses: [192.168.10.1/24] addresses: [192.168.100.1/24]
dhcp4: false dhcp4: false
optional: true optional: true

View File

@ -14,7 +14,7 @@ network:
# LAN — Interface 2 VirtualBox (Internal Network ou Host-Only) # LAN — Interface 2 VirtualBox (Internal Network ou Host-Only)
enp0s8: enp0s8:
addresses: [192.168.10.1/24] addresses: [192.168.100.1/24]
dhcp4: false dhcp4: false
optional: true optional: true

View File

@ -64,7 +64,7 @@ network:
br-lan: br-lan:
interfaces: [] interfaces: []
addresses: addresses:
- 192.168.10.1/24 - 192.168.1.1/24
dhcp4: false dhcp4: false
optional: true optional: true
parameters: parameters:

View File

@ -63,7 +63,7 @@ network:
bridges: bridges:
br-lan: br-lan:
interfaces: [enp0s8] interfaces: [enp0s8]
addresses: [192.168.10.1/24] addresses: [192.168.1.1/24]
dhcp4: false dhcp4: false
parameters: parameters:
stp: false stp: false

View File

@ -1,11 +0,0 @@
# Android / Gradle build artifacts
.gradle/
build/
app/build/
local.properties
*.apk
*.aab
*.keystore
*.jks
.idea/
captures/

View File

@ -1,74 +0,0 @@
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
# SecuBox Android ToolBox client (#531)
One-tap **R3 onboarding** for the VILLAGE3B cabine : install the CA,
import the WireGuard profile, verify the tunnel, then open the live
*cartographie sociale*. Replaces the manual Android tutorial.
## Flow (manual path)
1. **Discover** — scan the kbin QR or type the booth host (`kbin.gk2.secubox.in`).
2. **Install CA** — downloads `/wg/ca.crt`, launches the Android cert-install intent (`KeyChain.createInstallIntent`).
3. **Import profile** — downloads `/wg/profile/new`, hands the `.conf` to the WireGuard app via `FileProvider` + `ACTION_VIEW`.
4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓".
5. **Live metrics** — opens `/social/me` (cartographie sociale).
## Root path — real zero-tap, fully automated (#538, #551, #558)
On a **rooted** device the app onboards with **zero taps**, two ways:
- **On launch** — auto-detects root and runs the silent sequence immediately
every launch (no gate), retrying reachability while WiFi/tunnel settle.
- **On boot** — a `BOOT_COMPLETED` receiver starts a short foreground service
(`OnboardService`) that runs the same silent sequence **without opening the
app**, then stops. After one reboot the device self-onboards.
The **⚡ Installation automatique (root)** button remains as a manual
re-trigger. Two interactions are **mandated by Android and unavoidable** for
any app: the sideload install confirm ("install unknown apps") and the
first-time superuser (Magisk/su) grant prompt. Everything after those is
zero-tap. Steps:
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
`subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of
the trust store over `/system/etc/security/cacerts` (+ the conscrypt
APEX path on Android 14), restoring the SELinux context
(`u:object_r:system_security_cacerts_file:s0`). **Every** app trusts the
cabine CA — not just user-CA opt-in apps. Reversible via `umount`.
2. **Native WireGuard** — if the kernel has the WireGuard module + `wg`/`ip`,
brings the tunnel up natively (`ip link add … type wireguard` + `wg set`),
no WireGuard app required.
3. **Auto R3 verify** — polls `/wg/r3-check`.
**Fallback** — if the kernel lacks WireGuard, the root path installs the
system CA then hands off to the manual WireGuard-app flow (steps 35 above).
All root actions are **gated behind the explicit tap** — nothing runs as
root without the operator choosing root mode on their own device.
See `RootShell.kt` (su wrapper) and `RootOnboard.kt` (silent sequence).
## Build
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
- **GitHub Actions** `build-android-apk.yml` → debug APK artifact.
Locally (with Android SDK + Gradle 8.9 + JDK 17):
```bash
cd clients/android-toolbox
gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk
```
## Constraints (MVP)
- Android 11+ restricts **user CA trust** ; the *manual* path launches the
install intent + guides the confirm step. Browsers on the device need the
CA trusted for the mitm R3 break — this is the known Android limitation on
non-rooted devices. **Rooted devices bypass it entirely** via the system
CA install (see Root path above).
- The *manual* path imports the WireGuard profile via the **official
WireGuard app** (no embedded tunnel) — most reliable, no extra native
deps. The *root* path brings the tunnel up natively with the kernel module.
- Debug APK is self-signed (sideload). Release signing (published
fingerprint, served from the toolbox) is a follow-up needing a keystore
secret in CI.
## Tech
Kotlin + Jetpack Compose, minSdk 26 / targetSdk 34. API client is plain
`HttpURLConnection` (no Retrofit/OkHttp) to keep deps + CI minimal.
Package `in.secubox.toolbox`. License `LicenseRef-CMSD-1.0`.

View File

@ -1,49 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "in.secubox.toolbox"
compileSdk = 34
defaultConfig {
applicationId = "in.secubox.toolbox"
minSdk = 26
targetSdk = 34
versionCode = 4
versionName = "0.4.0"
}
buildTypes {
release {
isMinifyEnabled = false
// Signed in CI with a published-fingerprint key (sideload APK,
// no Play Store). Debug builds are self-signed by the SDK.
signingConfig = signingConfigs.findByName("release")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
buildFeatures { compose = true }
composeOptions { kotlinCompilerExtensionVersion = "1.5.14" }
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.06.00")
implementation(composeBom)
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.activity:activity-compose:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
// No Retrofit/OkHttp — the API client uses HttpURLConnection to keep
// the dependency graph (and CI) minimal.
}

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- #558 full-auto: run the silent onboarding on device boot, no app open. -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Query the WireGuard app so we can hand it the generated profile. -->
<queries>
<package android:name="com.wireguard.android" />
</queries>
<application
android:allowBackup="false"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.SecuBoxToolBox"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- #558 : boot-completed → start the onboarding foreground service
so a rooted device self-onboards with zero taps after a reboot. -->
<receiver
android:name=".BootReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".OnboardService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Silent R3 onboarding on a rooted, operator-owned cabine device" />
</service>
<!-- FileProvider to share the downloaded CA + WG .conf with the
system cert installer / the WireGuard app. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@ -1,23 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// #558 — boot-completed → kick the onboarding foreground service so a
// rooted, operator-owned cabine device self-onboards with zero taps after
// a reboot (no need to open the app).
package `in`.secubox.toolbox
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val a = intent?.action ?: return
if (a == Intent.ACTION_BOOT_COMPLETED || a == Intent.ACTION_LOCKED_BOOT_COMPLETED) {
ContextCompat.startForegroundService(
context, Intent(context, OnboardService::class.java),
)
}
}
}

View File

@ -1,323 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// SecuBox-Deb :: Android ToolBox client (#531)
// One-tap R3 onboarding : discover -> install CA -> import WG profile ->
// verify tunnel -> live cartographie sociale. Replaces the manual
// multi-step Android tutorial.
package `in`.secubox.toolbox
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.security.KeyChain
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private val Cosmos = Color(0xFF0A0A0F)
private val Gold = Color(0xFFC9A84C)
private val Cyan = Color(0xFF00D4FF)
private val Matrix = Color(0xFF00FF41)
private val Cinnabar = Color(0xFFE63946)
private val TextPrimary = Color(0xFFE8E6D9)
enum class Step { Discover, RootAuto, InstallCa, ImportProfile, Verify, Done }
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { OnboardApp() }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OnboardApp() {
val ctx = androidx.compose.ui.platform.LocalContext.current
val scope = rememberCoroutineScope()
var host by remember { mutableStateOf("kbin.gk2.secubox.in") }
var step by remember { mutableStateOf(Step.Discover) }
var status by remember { mutableStateOf("") }
var busy by remember { mutableStateOf(false) }
var onTunnel by remember { mutableStateOf(false) }
var peerIp by remember { mutableStateOf<String?>(null) }
val api = remember(host) { ToolboxApi(host) }
var rootAvail by remember { mutableStateOf(false) }
val rootLog = remember { mutableStateListOf<String>() }
val prefs = remember {
ctx.getSharedPreferences("secubox-toolbox", android.content.Context.MODE_PRIVATE)
}
var autoTried by remember { mutableStateOf(false) }
// The whole root-mode silent run, reused by the ⚡ button AND the
// zero-tap auto-launch (#551/#558). NO onboarded gate — it auto-runs
// every launch (idempotent: re-asserts CA + WG). Reachability is
// RETRIED so a WiFi/tunnel race at launch doesn't kill the auto-run.
val runRootAuto: () -> Unit = {
busy = true; status = ""; rootLog.clear()
scope.launch {
// poll reachability up to ~9 s (network may still be settling)
var ok = false
for (attempt in 1..6) {
ok = withContext(Dispatchers.IO) { api.reachable() }
if (ok) break
status = "Recherche de la borne… ($attempt)"
kotlinx.coroutines.delay(1500)
}
if (!ok) {
busy = false; status = "Borne injoignable — vérifie le réseau."
} else {
step = Step.RootAuto
val onb = RootOnboard(api, ctx.cacheDir, ctx.filesDir)
val out = withContext(Dispatchers.IO) {
onb.runSilent { line -> scope.launch(Dispatchers.Main) { rootLog.add(line) } }
}
busy = false
onTunnel = out.verified
// #683 — surface kbin Tor egress status (anonymised exit) if on.
rootLog.add(withContext(Dispatchers.IO) {
val t = api.torStatus()
when {
t == null -> "• Statut Tor : indisponible"
!t.optBoolean("tor_mode", false) -> "• Mode Tor : inactif"
t.optBoolean("running", false) ->
"🧅 Mode Tor ACTIF — sortie anonymisée${t.optString("exit_ip", "").let { if (it.isNotBlank() && it != "null") " ($it)" else "" }}"
else -> "🧅 Mode Tor activé — tunnel Tor en démarrage…"
}
})
when {
out.verified -> step = Step.Done
out.wgViaApp -> { step = Step.ImportProfile
status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." }
else -> { step = Step.Verify
status = "Active le tunnel puis vérifie." }
}
}
}
}
// Detect root once, off the main thread.
LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } }
// Zero-tap (#558): on a rooted device, auto-run the silent onboarding
// on every launch — no gate. (Boot-time auto-run is handled by
// BootReceiver + OnboardService so it runs without opening the app.)
LaunchedEffect(rootAvail) {
if (rootAvail && !autoTried && step == Step.Discover) {
autoTried = true
runRootAuto()
}
}
MaterialTheme(colorScheme = darkColorScheme(
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
onBackground = TextPrimary, onSurface = TextPrimary,
)) {
Surface(Modifier.fillMaxSize(), color = Cosmos) {
Column(
Modifier.fillMaxSize().padding(20.dp).verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("📡 VILLAGE3B", color = Gold, fontSize = 26.sp, fontWeight = FontWeight.Bold)
Text("ToolBoX — installation R3", color = TextPrimary, fontSize = 14.sp)
Spacer(Modifier.height(20.dp))
Stepper(step)
Spacer(Modifier.height(20.dp))
when (step) {
Step.Discover -> {
OutlinedTextField(
value = host, onValueChange = { host = it },
label = { Text("Borne (kbin…)") }, singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(8.dp))
Text("Scanne le QR de la cabine ou saisis l'adresse, puis Suivant.",
color = TextPrimary, fontSize = 12.sp)
Spacer(Modifier.height(16.dp))
BigButton("Suivant", busy) {
busy = true; status = "Vérification de la borne…"
scope.launch {
val ok = withContext(Dispatchers.IO) { api.reachable() }
busy = false
if (ok) { step = Step.InstallCa; status = "" }
else status = "Borne injoignable — vérifie l'adresse / le réseau."
}
}
if (rootAvail) {
Spacer(Modifier.height(10.dp))
Text("🔓 Root détecté — l'installation se lance automatiquement. " +
"Tu peux aussi la relancer ici.",
color = Matrix, fontSize = 12.sp)
Spacer(Modifier.height(6.dp))
OutlinedButton(onClick = runRootAuto, modifier = Modifier.fillMaxWidth(),
border = BorderStroke(1.dp, Matrix),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Matrix)) {
Text("⚡ Installation automatique (root)", fontWeight = FontWeight.Bold)
}
}
}
Step.RootAuto -> {
StepBody("Installation automatique (root)",
"CA système + tunnel WireGuard, sans intervention.")
Surface(color = Color(0xFF0E0E15), shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp)) {
rootLog.forEach { line ->
Text(line, color = if (line.startsWith("")) Cinnabar
else if (line.startsWith("")) Matrix else TextPrimary,
fontSize = 12.sp, fontFamily = FontFamily.Monospace)
}
if (busy) {
Spacer(Modifier.height(6.dp))
CircularProgressIndicator(Modifier.size(18.dp), color = Gold, strokeWidth = 2.dp)
}
}
}
}
Step.InstallCa -> {
StepBody("1 · Installer le certificat (CA R3)",
"Le certificat permet l'analyse TLS de la cabine. " +
"Android te demandera de confirmer (Paramètres → Sécurité → " +
"Certificat utilisateur).")
BigButton("Installer le certificat", busy) {
busy = true
scope.launch {
try {
val ca = withContext(Dispatchers.IO) { api.downloadCa(ctx.cacheDir) }
val der = ca.readBytes()
val intent = KeyChain.createInstallIntent().apply {
putExtra(KeyChain.EXTRA_CERTIFICATE, der)
putExtra(KeyChain.EXTRA_NAME, "VILLAGE3B ToolBoX CA")
}
ctx.startActivity(intent)
status = "Confirme l'installation dans Android, puis Suivant."
} catch (e: Exception) {
status = "Échec téléchargement CA : ${e.message}"
} finally { busy = false }
}
}
Spacer(Modifier.height(8.dp))
TextButton(onClick = {
ctx.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
}) { Text("Ouvrir Paramètres sécurité", color = Cyan) }
Spacer(Modifier.height(8.dp))
BigButton("Suivant", false) { step = Step.ImportProfile; status = "" }
}
Step.ImportProfile -> {
StepBody("2 · Importer le profil WireGuard",
"On génère un profil dédié et on l'ouvre dans l'app WireGuard. " +
"Active le tunnel dans WireGuard, puis reviens ici.")
BigButton("Importer dans WireGuard", busy) {
busy = true
scope.launch {
try {
val conf = withContext(Dispatchers.IO) { api.downloadProfile(ctx.cacheDir) }
val uri = FileProvider.getUriForFile(
ctx, "${ctx.packageName}.fileprovider", conf)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/octet-stream")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
setPackage("com.wireguard.android")
}
try { ctx.startActivity(intent) }
catch (_: Exception) {
// WireGuard not installed -> open Play / generic chooser.
ctx.startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=com.wireguard.android")))
status = "Installe l'app WireGuard puis réessaie."
}
} catch (e: Exception) {
status = "Échec profil : ${e.message}"
} finally { busy = false }
}
}
Spacer(Modifier.height(8.dp))
BigButton("Suivant", false) { step = Step.Verify; status = "" }
}
Step.Verify -> {
StepBody("3 · Vérifier le tunnel R3",
"Active le tunnel dans WireGuard, puis vérifie.")
BigButton("Vérifier", busy) {
busy = true; status = "Vérification…"
scope.launch {
val (t, ip) = withContext(Dispatchers.IO) { api.r3Check() }
busy = false; onTunnel = t; peerIp = ip
if (t) { step = Step.Done; status = "" }
else status = "Pas encore sur le tunnel — active WireGuard puis réessaie."
}
}
}
Step.Done -> {
Icon(Icons.Filled.CheckCircle, null, tint = Matrix, modifier = Modifier.size(56.dp))
Text("Tunnel R3 actif ✓", color = Matrix, fontSize = 20.sp, fontWeight = FontWeight.Bold)
peerIp?.let { Text("pair : $it", color = TextPrimary, fontSize = 12.sp) }
Spacer(Modifier.height(20.dp))
BigButton("🕸️ Voir ma cartographie sociale", false) {
ctx.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(api.socialMeUrl)))
}
}
}
if (status.isNotBlank()) {
Spacer(Modifier.height(16.dp))
Text(status, color = if (status.contains("Échec") || status.contains("injoignable")) Cinnabar else Cyan,
fontSize = 13.sp)
}
}
}
}
}
@Composable
private fun Stepper(cur: Step) {
val steps = listOf(Step.Discover, Step.InstallCa, Step.ImportProfile, Step.Verify, Step.Done)
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
steps.forEach { s ->
val done = s.ordinal < cur.ordinal
val active = s == cur
Box(Modifier.size(if (active) 14.dp else 10.dp)) {
Surface(shape = MaterialTheme.shapes.small,
color = when { done -> Matrix; active -> Gold; else -> Color(0xFF333333) },
modifier = Modifier.fillMaxSize()) {}
}
}
}
}
@Composable
private fun StepBody(title: String, body: String) {
Text(title, color = Gold, fontSize = 16.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Text(body, color = TextPrimary, fontSize = 13.sp)
Spacer(Modifier.height(16.dp))
}
@Composable
private fun BigButton(label: String, busy: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = !busy, modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = Gold, contentColor = Cosmos)) {
if (busy) CircularProgressIndicator(Modifier.size(18.dp), color = Cosmos, strokeWidth = 2.dp)
else Text(label, fontWeight = FontWeight.Bold)
}
}

View File

@ -1,77 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// #558 — full-auto onboarding service. Started on boot (BootReceiver). On a
// rooted device it runs the silent R3 onboarding (system CA + native WG +
// verify) with zero taps, retrying reachability while the network settles,
// then stops itself. Non-root / unreachable → it just stops (the launcher
// activity remains the manual path).
package `in`.secubox.toolbox
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class OnboardService : Service() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val CHAN = "sbx-onboard"
private val NID = 4201
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NID, buildNotification())
scope.launch {
try { runOnce() } finally { stopSelf() }
}
return START_NOT_STICKY
}
private suspend fun runOnce() {
// root is the precondition for the silent path; bail quietly otherwise.
if (!RootShell.available()) return
val host = getSharedPreferences("secubox-toolbox", Context.MODE_PRIVATE)
.getString("host", null) ?: "kbin.gk2.secubox.in"
val api = ToolboxApi(host)
// network may still be coming up after boot — retry ~30 s.
var ok = false
for (i in 1..15) {
ok = api.reachable()
if (ok) break
kotlinx.coroutines.delay(2000)
}
if (!ok) return
RootOnboard(api, cacheDir, filesDir).runSilent { /* headless: no UI log */ }
}
private fun buildNotification(): Notification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val nm = getSystemService(NotificationManager::class.java)
nm?.createNotificationChannel(
NotificationChannel(CHAN, "SecuBox onboarding",
NotificationManager.IMPORTANCE_LOW),
)
}
val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
Notification.Builder(this, CHAN) else @Suppress("DEPRECATION") Notification.Builder(this)
return b.setContentTitle("VILLAGE3B")
.setContentText("Activation R3 automatique…")
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.build()
}
override fun onDestroy() {
super.onDestroy()
scope.coroutineContext[kotlinx.coroutines.Job]?.cancel()
}
}

View File

@ -1,170 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// Root-mode fully-automated silent R3 onboarding (#538).
// All actions are gated behind an explicit "root auto" tap in the UI.
package `in`.secubox.toolbox
import java.io.File
import java.security.MessageDigest
import java.security.cert.CertificateFactory
class RootOnboard(
private val api: ToolboxApi,
private val cacheDir: File,
// #683: app-internal storage for the STABLE WG identity (survives reboot).
// Defaults to cacheDir so older call sites still compile, but real callers
// pass filesDir so the identity persists instead of churning each boot.
private val filesDir: File = cacheDir,
) {
/** A line appended to the on-screen log during the silent run. */
fun interface Logger { fun log(line: String) }
data class Outcome(val caInstalled: Boolean, val wgUp: Boolean, val verified: Boolean,
val wgViaApp: Boolean)
// ── system CA install (silent, root) ──
/**
* OpenSSL `subject_hash_old` (pre-1.0 hash) computed WITHOUT openssl :
* MD5 of the DER-encoded subject name, first 4 bytes as a uint32
* little-endian, formatted "%08x". This is the filename the Android
* system cacerts store uses (<hash>.0).
*/
fun subjectHashOld(pem: ByteArray): String {
val cf = CertificateFactory.getInstance("X.509")
val cert = cf.generateCertificate(pem.inputStream()) as java.security.cert.X509Certificate
val subjectDer = cert.subjectX500Principal.encoded
val md5 = MessageDigest.getInstance("MD5").digest(subjectDer)
val h = (md5[0].toLong() and 0xff) or
((md5[1].toLong() and 0xff) shl 8) or
((md5[2].toLong() and 0xff) shl 16) or
((md5[3].toLong() and 0xff) shl 24)
return String.format("%08x", h)
}
/**
* Install the CA into the SYSTEM trust store so EVERY app (not just
* those opting into user CAs) trusts it. Uses the bind-mount-over-
* cacerts technique that works on Android 1014 (incl. the conscrypt
* APEX). Non-persistent across reboot fine for a temporary cabine
* diagnostic; the app can also unmount to revert.
*/
fun installCaSystem(log: Logger): Boolean {
log.log("• Téléchargement du CA…")
val pem = api.download("/wg/ca.pem", "village3b-ca.pem", cacheDir).readBytes()
val hash = subjectHashOld(pem)
log.log("• CA hash : $hash.0")
val local = File(cacheDir, "$hash.0").apply { writeBytes(pem) }
// Push the cert to a root-readable scratch path, then bind-mount a
// populated copy of the system store over the live cacerts dir.
val pushed = "/data/local/tmp/sbx-$hash.0"
val push = RootShell.install(local, pushed, "644")
if (!push.ok) { log.log("✗ push échoué : ${push.err.trim()}"); return false }
val r = RootShell.runScript(
"set -e",
"CERT_DIR=/system/etc/security/cacerts",
"TMP=/data/local/tmp/sbx-cacerts",
"rm -rf \$TMP; mkdir -p \$TMP",
// seed with the existing system + APEX certs so nothing is lost
"cp -f \$CERT_DIR/* \$TMP/ 2>/dev/null || true",
"cp -f /apex/com.android.conscrypt/cacerts/* \$TMP/ 2>/dev/null || true",
"cp -f $pushed \$TMP/$hash.0",
"chmod 644 \$TMP/* 2>/dev/null || true",
"chown 0:0 \$TMP/* 2>/dev/null || true",
"chcon u:object_r:system_security_cacerts_file:s0 \$TMP/* 2>/dev/null || true",
// bind-mount over the live store (and the APEX path on 14)
"mount -o bind \$TMP \$CERT_DIR",
"[ -d /apex/com.android.conscrypt/cacerts ] && mount -o bind \$TMP /apex/com.android.conscrypt/cacerts 2>/dev/null || true",
"echo OK",
)
if (r.ok && r.out.contains("OK")) {
log.log("✓ CA installé dans le magasin système (toutes les apps le font confiance)")
return true
}
log.log("✗ install CA système : ${r.err.trim().ifBlank { r.out.trim() }}")
return false
}
fun removeCaSystem(log: Logger): Boolean {
val r = RootShell.runScript(
"umount /system/etc/security/cacerts 2>/dev/null || true",
"umount /apex/com.android.conscrypt/cacerts 2>/dev/null || true",
"echo OK",
)
log.log(if (r.ok) "✓ CA système retiré (démonté)" else "✗ démontage : ${r.err.trim()}")
return r.ok
}
// ── WireGuard bring-up ──
/** Parse the wg-quick .conf into the fields we need. */
private data class WgConf(val privKey: String, val address: String,
val pubKey: String, val endpoint: String, val allowed: String)
private fun parse(conf: String): WgConf? {
var pk = ""; var addr = ""; var pub = ""; var ep = ""; var aip = ""
conf.lineSequence().forEach { raw ->
val l = raw.trim()
when {
l.startsWith("PrivateKey", true) -> pk = l.substringAfter("=").trim()
l.startsWith("Address", true) -> addr = l.substringAfter("=").trim()
l.startsWith("PublicKey", true) -> pub = l.substringAfter("=").trim()
l.startsWith("Endpoint", true) -> ep = l.substringAfter("=").trim()
l.startsWith("AllowedIPs", true) -> aip = l.substringAfter("=").trim()
}
}
return if (pk.isNotBlank() && pub.isNotBlank() && ep.isNotBlank()) WgConf(pk, addr, pub, ep, aip) else null
}
/**
* Bring the tunnel up natively with root IF the kernel has WireGuard
* + `wg`/`ip`. Returns true on success ; false means the caller
* should fall back to the WireGuard-app handoff.
*/
fun setupWireguardRoot(log: Logger): Boolean {
if (!RootShell.hasKernelWireguard()) {
log.log("• Noyau sans module WireGuard — bascule sur l'app WireGuard")
return false
}
log.log("• Profil WireGuard (identité stable)…")
// #683: reuse the persisted keypair so the device keeps ONE identity
// across reboots (no more stats reset to a fresh empty hash each boot).
val conf = api.persistentProfile(filesDir).readText()
val wg = parse(conf) ?: run { log.log("✗ profil illisible"); return false }
val iface = "wg-village3b"
val r = RootShell.runScript(
"set -e",
"ip link del $iface 2>/dev/null || true",
"ip link add $iface type wireguard",
"echo '${wg.privKey}' > /data/local/tmp/sbx-wg.key && chmod 600 /data/local/tmp/sbx-wg.key",
"wg set $iface private-key /data/local/tmp/sbx-wg.key peer ${wg.pubKey} endpoint ${wg.endpoint} allowed-ips ${wg.allowed.ifBlank { "0.0.0.0/0" }} persistent-keepalive 25",
"rm -f /data/local/tmp/sbx-wg.key",
if (wg.address.isNotBlank()) "ip addr add ${wg.address} dev $iface 2>/dev/null || true" else ":",
"ip link set $iface up",
"for n in ${wg.allowed.replace(",", " ")}; do ip route replace \$n dev $iface 2>/dev/null || true; done",
"echo OK",
)
if (r.ok && r.out.contains("OK")) { log.log("✓ Tunnel $iface actif (root, natif)"); return true }
log.log("✗ WG natif : ${r.err.trim().ifBlank { r.out.trim() }} — bascule sur l'app")
return false
}
/** Run the whole silent sequence. Blocking — call off-main. */
fun runSilent(log: Logger): Outcome {
val ca = installCaSystem(log)
val wgRoot = setupWireguardRoot(log)
var verified = false
if (wgRoot) {
log.log("• Vérification R3…")
Thread.sleep(1500)
val (t, ip) = api.r3Check()
verified = t
log.log(if (t) "✓ Tunnel R3 confirmé (${ip ?: "?"})" else "• Pas encore confirmé — réessaie la vérification")
}
return Outcome(caInstalled = ca, wgUp = wgRoot, verified = verified, wgViaApp = !wgRoot)
}
}

View File

@ -1,63 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
package `in`.secubox.toolbox
import java.io.File
/**
* Thin wrapper around `su` for the root-mode silent onboarding (#538).
* Every action is gated behind an explicit user tap in the UI nothing
* runs as root without the operator choosing root mode on their own
* device.
*/
object RootShell {
data class Result(val code: Int, val out: String, val err: String) {
val ok get() = code == 0
}
/** True if a `su` binary is on PATH and grants a root shell. */
fun available(): Boolean = try {
run("id -u").out.trim() == "0"
} catch (_: Exception) { false }
/** Run a single command in a root shell. Blocking — call off-main. */
fun run(cmd: String): Result {
val p = ProcessBuilder("su", "-c", cmd)
.redirectErrorStream(false)
.start()
val out = p.inputStream.bufferedReader().readText()
val err = p.errorStream.bufferedReader().readText()
val code = p.waitFor()
return Result(code, out, err)
}
/** Run several commands in ONE root shell (atomic-ish, keeps remount). */
fun runScript(vararg lines: String): Result {
val p = ProcessBuilder("su").redirectErrorStream(false).start()
p.outputStream.bufferedWriter().use { w ->
lines.forEach { w.write(it); w.write("\n") }
w.write("exit $?\n")
}
val out = p.inputStream.bufferedReader().readText()
val err = p.errorStream.bufferedReader().readText()
val code = p.waitFor()
return Result(code, out, err)
}
/** Push a local file to a root-owned path via cat (avoids cp quirks). */
fun install(src: File, destPath: String, mode: String = "644"): Result {
val b64 = android.util.Base64.encodeToString(src.readBytes(), android.util.Base64.NO_WRAP)
return runScript(
"echo '$b64' | base64 -d > '$destPath'",
"chmod $mode '$destPath'",
)
}
/** Kernel has WireGuard + the `wg` tool available to root? */
fun hasKernelWireguard(): Boolean = try {
val w = run("command -v wg || ls /system/*bin/wg 2>/dev/null")
val ip = run("command -v ip")
w.out.isNotBlank() && ip.out.isNotBlank()
} catch (_: Exception) { false }
}

View File

@ -1,106 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
package `in`.secubox.toolbox
import org.json.JSONObject
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
/**
* Minimal HTTP client for the SecuBox ToolBox R3 endpoints. Uses
* HttpURLConnection (no Retrofit/OkHttp) to keep the dependency graph
* and CI minimal. All calls are blocking invoke off the main thread.
*
* Endpoints (served on the kbin vhost, e.g. kbin.gk2.secubox.in) :
* GET /wg/ca.crt -> CA root cert (Android DER/PEM)
* GET /wg/profile/new -> wg-quick .conf (one fresh peer per call)
* GET /wg/r3-check -> {"tunnel": bool, "peer_ip": "10.99.1.x"}
* GET /social/me -> per-client cartographie sociale (web view)
*/
class ToolboxApi(rawHost: String) {
// Accept "kbin.gk2.secubox.in", "https://kbin…", trailing slashes…
val base: String = rawHost.trim()
.removePrefix("https://").removePrefix("http://")
.trim('/')
.let { "https://$it" }
val socialMeUrl: String get() = "$base/social/me"
private fun open(path: String): HttpURLConnection =
(URL("$base$path").openConnection() as HttpURLConnection).apply {
connectTimeout = 8000
readTimeout = 12000
setRequestProperty("User-Agent", "secubox-toolbox-android/0.1")
instanceFollowRedirects = true
}
/** Download a file (CA or WG profile) into the app cache, return it. */
fun download(path: String, outName: String, cacheDir: File): File {
val c = open(path)
try {
if (c.responseCode !in 200..299)
throw RuntimeException("HTTP ${c.responseCode} for $path")
val out = File(cacheDir, outName)
c.inputStream.use { input -> out.outputStream().use { input.copyTo(it) } }
return out
} finally { c.disconnect() }
}
fun downloadCa(cacheDir: File): File = download("/wg/ca.crt", "village3b-ca.crt", cacheDir)
fun downloadProfile(cacheDir: File): File = download("/wg/profile/new", "village3b-toolbox.conf", cacheDir)
/**
* The device's STABLE WireGuard identity (#683 lost-referrer fix).
*
* `/wg/profile/new` mints a FRESH keypair on every call. The onboarding
* runs on every boot, so calling it each time gave the device a NEW pubkey
* new sha256(pubkey) identity hash its stats/social history reset to an
* empty bucket on every reboot/reconnect. Here we fetch a peer ONCE and
* persist the .conf in app-internal `filesDir` (survives reboots, unlike the
* evictable cacheDir). Every later call reuses the SAME keypair SAME
* identity the device keeps one continuous history.
*
* Survives reboot/reconnect/app-restart. (Reinstall still wipes filesDir;
* cross-reinstall persistence would need allowBackup kept off for CSPN.)
*/
fun persistentProfile(filesDir: File): File {
val stored = File(filesDir, "identity-wg.conf")
if (stored.exists() && stored.length() > 0L &&
stored.readText().contains("PrivateKey", ignoreCase = true)) {
return stored
}
val fresh = download("/wg/profile/new", "identity-wg.conf.tmp", filesDir)
fresh.copyTo(stored, overwrite = true)
fresh.delete()
return stored
}
/** kbin Tor egress status for the client UI (read-only, kbin-safe). */
fun torStatus(): JSONObject? {
val c = open("/wg/tor-status")
return try {
if (c.responseCode !in 200..299) null
else JSONObject(c.inputStream.bufferedReader().readText())
} catch (_: Exception) { null } finally { c.disconnect() }
}
/** R3 tunnel status. Returns (onTunnel, peerIp?). */
fun r3Check(): Pair<Boolean, String?> {
val c = open("/wg/r3-check")
try {
if (c.responseCode !in 200..299) return false to null
val body = c.inputStream.bufferedReader().readText()
val j = JSONObject(body)
return j.optBoolean("tunnel", false) to j.optString("peer_ip", null)
} catch (_: Exception) {
return false to null
} finally { c.disconnect() }
}
/** Cheap reachability probe for the discover step. */
fun reachable(): Boolean = try {
val c = open("/wg/r3-check"); val ok = c.responseCode in 200..499; c.disconnect(); ok
} catch (_: Exception) { false }
}

View File

@ -1,8 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportHeight="108">
<path android:fillColor="#C9A84C"
android:pathData="M54,40c-10,0 -19,6 -24,14c5,8 14,14 24,14s19,-6 24,-14c-5,-8 -14,-14 -24,-14zM54,64a10,10 0 1,1 0,-20a10,10 0 0,1 0,20z" />
<path android:fillColor="#00D4FF"
android:pathData="M54,49a5,5 0 1,0 0,10a5,5 0 0,0 0,-10z" />
</vector>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#0A0A0F</color>
</resources>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<resources>
<string name="app_name">VILLAGE3B ToolBoX</string>
</resources>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<resources>
<style name="Theme.SecuBoxToolBox" parent="android:Theme.Material.NoActionBar">
<item name="android:statusBarColor">#0A0A0F</item>
<item name="android:navigationBarColor">#0A0A0F</item>
</style>
</resources>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<paths>
<cache-path name="cache" path="." />
</paths>

View File

@ -1,5 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
plugins {
id("com.android.application") version "8.5.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
}

View File

@ -1,5 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true

View File

@ -1,18 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// SecuBox-Deb :: Android ToolBox client (#531)
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "SecuBoxToolBox"
include(":app")

View File

@ -1,5 +0,0 @@
# build artefacts
*.xpi
*.zip
*.crx
web-ext-artifacts/

View File

@ -1,110 +0,0 @@
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
# SecuBox ToolBoX — browser extension (Cartographie sociale, #532)
A WebExtension (Firefox `.xpi` + Chromium MV3) that **emancipates** the R3
toolbox live tracker analysis into the browser: instead of only seeing the
*cartographie sociale* on `kbin/social/me`, a toolbar badge ticks up as
trackers fire, and a popup shows who is watching you — live.
Sibling of [`clients/android-toolbox/`](../android-toolbox/). Talks **only**
to your cabine over the R3 tunnel — no third-party calls.
## What it does
- **Pairing** — calls `/social/me` over the tunnel, which 303-redirects to
`/social/{token}`; the extension reads the minted HMAC token from the
final URL. Anonymous (rotating `mac_hash`), no account. Manual token entry
available in the options page.
- **Live badge** — the toolbar icon shows the live tracker count for the
session (polled once a minute). Colour escalates: gold → 🟥 anti-bot
present → 🟪 operator-grade present.
- **Popup** — four stat tiles (trackers / sites / anti-bot / operator-grade),
a dependency-free **mini Round-Eye graph** (device centre, trackers on the
ring, radius by hits, colour by tier), and a top-tracker list with CDN
(12.A) / anti-bot (12.B) / operator-grade (12.C) tags.
- **Actions***Cartographie complète* (opens the full d3 view at
`/social/{token}`), *Rapport PDF* (`/social/report/{token}.pdf`), and
*Effacer mes données* (RGPD art. 17 wipe → `POST /social/wipe/{token}`).
## Install
Published release `.xpi` (downloadable directly):
```
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:
```
https://kbin.<board>.secubox.in/wg/toolbox.xpi
```
The kbin onboard panel exposes a **🧩 Extension navigateur (cartographie)**
button. When a local build is present the cabine serves it; otherwise it
302-redirects to the **tag-pinned** release asset above. The webext release
is published `make_latest:false` so it does not steal the repo "Latest"
pointer from the Android APK release (whose endpoint resolves via
`/releases/latest/download/…`) — bump the tag in the `/wg/toolbox.xpi`
endpoint constant + `secubox-toolbox-fetch-xpi` when a new `webext-v*`
release is cut.
- **Firefox** — open the `.xpi`. A permanent install needs an AMO-signed
build (release CI step / `web-ext sign`); for development use
*about:debugging → Load Temporary Add-on*, or an ESR/Dev build with
`xpinstall.signatures.required=false`.
- **Linux Firefox (fast)** — one call grabs the `.xpi` and launches Firefox
with it loaded (via `web-ext run`, no signing needed):
```bash
clients/webext-toolbox/install-firefox-linux.sh # from kbin.gk2.secubox.in
clients/webext-toolbox/install-firefox-linux.sh --release # from the GitHub release
clients/webext-toolbox/install-firefox-linux.sh --local # from this checkout
```
- **Chromium** — load unpacked (`chrome://extensions` → Developer mode).
Ships rasterised PNG icons (`icons/icon-48/128.png`), so it loads as-is.
## Build
No bundler — the extension is plain JS/HTML/CSS. CI zips it:
- GitHub Actions `build-webext.yml``.xpi` artifact on push to `master` /
PRs touching `clients/webext-toolbox/**`; tagging `webext-v*` publishes the
`.xpi` as a release asset.
Locally:
```bash
cd clients/webext-toolbox
./build.sh # → secubox-toolbox-webext-<version>.xpi
```
## Files
| File | Role |
|------|------|
| `manifest.json` | MV3, cross-browser background (`service_worker` + `scripts`) |
| `api.js` | shared client over `/wg/r3-check`, `/social/*` |
| `background.js` | badge sync + silent re-pair (SW or event page) |
| `popup/` | live view, mini graph (`graph.js`), actions |
| `options/` | host / window / manual token |
## Cabine endpoints consumed
| Endpoint | Purpose |
|----------|---------|
| `/wg/r3-check` | tunnel presence indicator |
| `/social/me` | pair → mint token (303 → `/social/{token}`) |
| `/social/graph/{token}?since=` | per-session tracker graph JSON |
| `/social/wipe/{token}` | RGPD art. 17 erasure |
| `/social/{token}` | full d3 cartographie page |
| `/social/report/{token}.pdf` | bilingual PDF report |
## Notes
- No server-side CORS needed: an MV3 extension with `host_permissions` for
`*.secubox.in` fetches cross-origin from its background without CORS.
- MVP polls `/social/graph` and computes the delta client-side; a future
`GET /social/live/{token}` (SSE) can replace the poll. The deception-plane
*Poke/Emancipate* per-site control lands once #525 ships.
License `LicenseRef-CMSD-1.0`.

View File

@ -1,161 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// SecuBox-Deb :: webext-toolbox :: api
// Thin client over the R3 toolbox social endpoints. Shared by the
// background service worker and the popup. Cross-origin fetches are
// allowed because the extension holds host_permissions for the cabine
// vhosts — no server-side CORS needed.
// browser (Firefox promise API) || chrome (Chromium / FF MV3 SW)
const ext = globalThis.browser || globalThis.chrome;
const DEFAULTS = {
host: "kbin.gk2.secubox.in",
token: "",
since: 86400,
};
// base URL from a stored host (accept bare host or full origin)
function baseUrl(host) {
const h = (host || DEFAULTS.host).trim().replace(/\/+$/, "");
if (/^https?:\/\//i.test(h)) return h;
return `https://${h}`;
}
async function getConfig() {
const stored = await ext.storage.local.get(["host", "token", "since"]);
return { ...DEFAULTS, ...stored };
}
async function setConfig(patch) {
await ext.storage.local.set(patch);
return getConfig();
}
// Extract the HMAC token from a /social/{token} URL path.
function tokenFromUrl(url) {
try {
const u = new URL(url);
const m = u.pathname.match(/\/social\/([^/?#]+)/);
if (m && m[1] !== "me" && m[1].split(".").length === 4) return m[1];
} catch (_) {}
return null;
}
// Pair: hit /social/me over the tunnel; it 303-redirects to
// /social/{token}. fetch follows the redirect, so response.url carries
// the minted token. Returns the token or throws.
async function pair(host) {
const url = `${baseUrl(host)}/social/me`;
const resp = await fetch(url, { redirect: "follow", credentials: "omit" });
const tok = tokenFromUrl(resp.url);
if (!tok) throw new Error("pairing failed — not on the R3 tunnel?");
return tok;
}
// r3-check: is this client on the R3 tunnel right now?
async function r3Check(host) {
try {
const resp = await fetch(`${baseUrl(host)}/wg/r3-check`, { credentials: "omit" });
if (!resp.ok) return { tunnel: false, peer_ip: null };
return await resp.json();
} catch (_) {
return { tunnel: false, peer_ip: null };
}
}
// #683 — kbin Tor egress status (public, kbin-safe endpoint).
async function torStatus(host) {
try {
const resp = await fetch(`${baseUrl(host)}/wg/tor-status`, { credentials: "omit" });
if (!resp.ok) return { tor_mode: false };
return await resp.json();
} catch (_) {
return { tor_mode: false };
}
}
// graph: the per-session cartographie JSON. Throws on HTTP error so the
// caller can show "token expired — re-pair".
async function graph(host, token, since) {
const qs = new URLSearchParams({ since: String(since || DEFAULTS.since) });
const resp = await fetch(`${baseUrl(host)}/social/graph/${token}?${qs}`, {
credentials: "omit",
});
if (resp.status === 403 || resp.status === 404) {
throw new Error("token-expired");
}
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
}
// RGPD art.17 wipe.
async function wipe(host, token) {
const resp = await fetch(`${baseUrl(host)}/social/wipe/${token}`, {
method: "POST",
credentials: "omit",
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
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
// (7-day cached PNG, transparent 1×1 fallback) — no third-party call.
function faviconUrl(host, domain) {
return `${baseUrl(host)}/social/favicon/${encodeURIComponent(domain || "")}`;
}
function socialUrl(host, token) {
return `${baseUrl(host)}/social/${token}`;
}
function reportUrl(host, token) {
return `${baseUrl(host)}/social/report/${token}.pdf`;
}
const SbxApi = {
DEFAULTS,
ext,
baseUrl,
getConfig,
setConfig,
pair,
r3Check,
torStatus,
graph,
wipe,
ghost,
getAdminFilters,
setAdminFilters,
faviconUrl,
socialUrl,
reportUrl,
tokenFromUrl,
};
// Usable both as a classic background script (globalThis) and an ES-less
// service worker. No module syntax to stay loadable as plain script.
globalThis.SbxApi = SbxApi;

View File

@ -1,81 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// SecuBox-Deb :: webext-toolbox :: background
// Keeps the toolbar badge in sync with the live tracker count and
// re-pairs over the R3 tunnel when the token expires. Works as a
// Chromium MV3 service worker (importScripts) AND a Firefox event page
// (api.js preloaded via background.scripts).
if (typeof importScripts === "function") {
// Chromium service worker: api.js isn't auto-loaded, pull it in.
try { importScripts("api.js"); } catch (_) {}
}
// NB: do NOT declare `const ext` here — api.js already declares it in the
// shared script scope (event page) / worker global (importScripts), and a
// second `const ext` is a "redeclaration of const ext" SyntaxError that
// kills the whole background script. Use api.ext instead.
const api = globalThis.SbxApi;
const ALARM = "sbx-refresh";
const PERIOD_MIN = 1; // poll the cabine once a minute
function setBadge(text, color) {
try {
api.ext.action.setBadgeText({ text: text || "" });
if (color) ext.action.setBadgeBackgroundColor({ color });
} catch (_) {}
}
// Pull the graph, update the badge with the live tracker count. Auto
// re-pairs once if the stored token has expired.
async function refresh() {
const cfg = await api.getConfig();
if (!cfg.host) { setBadge("", "#6b6b7a"); return; }
let token = cfg.token;
const run = async (tok) => api.graph(cfg.host, tok, cfg.since);
try {
if (!token) token = await api.pair(cfg.host);
let data;
try {
data = await run(token);
} catch (e) {
if (String(e.message) === "token-expired") {
token = await api.pair(cfg.host); // one silent re-pair
data = await run(token);
} else throw e;
}
await api.setConfig({ token });
const n = (data.stats && data.stats.total_trackers) || 0;
// colour escalates with operator-grade / anti-bot presence
const opg = (data.stats && data.stats.opgrade_sites) || 0;
const ab = (data.stats && data.stats.antibot_sites) || 0;
const color = opg > 0 ? "#6e40c9" : ab > 0 ? "#e63946" : "#c9a84c";
setBadge(n > 999 ? "999+" : String(n), color);
await ext.storage.local.set({ lastStats: data.stats || {}, lastError: "" });
} catch (e) {
setBadge("!", "#6b6b7a");
await ext.storage.local.set({ lastError: String(e.message || e) });
}
}
api.ext.runtime.onInstalled.addListener(() => {
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
refresh();
});
api.ext.runtime.onStartup && api.ext.runtime.onStartup.addListener(() => {
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
refresh();
});
api.ext.alarms.onAlarm.addListener((a) => { if (a.name === ALARM) refresh(); });
// popup asks for an immediate refresh after pairing / config change
api.ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg && msg.type === "refresh") {
refresh().then(() => sendResponse({ ok: true }));
return true; // async response
}
});

View File

@ -1,26 +0,0 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# SecuBox ToolBoX Cartographie — build the unsigned .xpi (a zip of the
# extension dir). Firefox loads it as-is (temporary add-on / ESR with
# signatures off) ; a release build signs it via web-ext / AMO.
# Usage: ./build.sh → produces ./secubox-toolbox-webext-<version>.xpi
set -euo pipefail
cd "$(dirname "$0")"
VER=$(grep -oE '"version"[^,]*' manifest.json | grep -oE '[0-9.]+' | head -1)
OUT="secubox-toolbox-webext-${VER}.xpi"
rm -f "$OUT"
# -FS = sync (drop stale entries) ; exclude VCS, dotfiles, build script,
# any previously built artefact, docs, and the SVG icon source (only the
# rasterised PNGs are referenced by the manifest — keep SVG out of the
# package so Firefox never renders it in chrome UI).
zip -r -FS "$OUT" . \
-x '*.git*' '*/.*' 'build.sh' '*.xpi' 'README.md' 'icons/icon.svg' >/dev/null
echo "built $OUT ($(stat -c%s "$OUT" 2>/dev/null || stat -f%z "$OUT") bytes)"
echo "Firefox: about:debugging → This Firefox → Load Temporary Add-on → pick the .xpi (or manifest.json)."
echo "Permanent install needs signing (web-ext sign / AMO) or Dev/ESR with xpinstall.signatures.required=false."
echo "Chromium: action icons must be raster — rasterise icons/icon.svg to PNG before a Chromium store build."

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<rect width="128" height="128" rx="24" fill="#0a0a0f"/>
<!-- outer eye almond -->
<path d="M64 36 C92 36 114 64 114 64 C114 64 92 92 64 92 C36 92 14 64 14 64 C14 64 36 36 64 36 Z"
fill="none" stroke="#c9a84c" stroke-width="5"/>
<!-- iris -->
<circle cx="64" cy="64" r="20" fill="#0c0c12" stroke="#00ff41" stroke-width="4"/>
<circle cx="64" cy="64" r="7" fill="#00ff41"/>
<!-- tracker spokes -->
<g stroke="#6e40c9" stroke-width="3" opacity="0.8">
<line x1="64" y1="64" x2="104" y2="40"/>
<line x1="64" y1="64" x2="24" y2="40"/>
<line x1="64" y1="64" x2="100" y2="92"/>
</g>
<g fill="#00d4ff">
<circle cx="104" cy="40" r="5"/>
<circle cx="24" cy="40" r="5"/>
</g>
<circle cx="100" cy="92" r="5" fill="#e63946"/>
</svg>

Before

Width:  |  Height:  |  Size: 951 B

View File

@ -1,87 +0,0 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# SecuBox ToolBoX — Linux Firefox installer (#547)
# One call: grab the ToolBoX cartographie extension and launch Firefox with
# it loaded. Prefers `web-ext run` (temporary load, works unsigned — fastest)
# and falls back to opening the .xpi for the install prompt.
#
# Usage:
# ./install-firefox-linux.sh # from kbin.gk2.secubox.in
# ./install-firefox-linux.sh kbin.my.box # from another cabine host
# ./install-firefox-linux.sh --release # from the latest GitHub release
# ./install-firefox-linux.sh --local # build from this checkout (web-ext)
set -euo pipefail
DEFAULT_HOST="kbin.gk2.secubox.in"
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)"
say(){ printf '\033[1;36m▸\033[0m %s\n' "$*"; }
warn(){ printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
die(){ printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }
# ── resolve source ──
MODE="host"; HOST="$DEFAULT_HOST"; SRC_URL=""
case "${1:-}" in
--release) MODE="release"; SRC_URL="$RELEASE_URL" ;;
--local) MODE="local" ;;
"") SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
-*) die "unknown flag: $1 (use --release | --local | <host>)" ;;
*) HOST="$1"; SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
esac
# ── find a Firefox binary ──
FX=""
for c in firefox firefox-esr firefox-bin firefox-developer-edition; do
if command -v "$c" >/dev/null 2>&1; then FX="$c"; break; fi
done
if [ -z "$FX" ] && command -v flatpak >/dev/null 2>&1 \
&& flatpak info org.mozilla.firefox >/dev/null 2>&1; then
FX="flatpak run org.mozilla.firefox"
fi
[ -n "$FX" ] || die "no Firefox found (install firefox / firefox-esr, or flatpak org.mozilla.firefox)"
say "Firefox: $FX"
have_webext(){ command -v web-ext >/dev/null 2>&1 || command -v npx >/dev/null 2>&1; }
runwebext(){ if command -v web-ext >/dev/null 2>&1; then web-ext "$@"; else npx --yes web-ext "$@"; fi; }
# ── fastest path: web-ext run (temporary load, no signing needed) ──
if have_webext; then
SRCDIR=""
if [ "$MODE" = "local" ]; then
[ -f "$SELF_DIR/manifest.json" ] || die "--local: no manifest.json next to this script"
SRCDIR="$SELF_DIR"
else
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
say "Downloading extension from ${SRC_URL}"
curl -fsSL "$SRC_URL" -o "$TMP/sbx.xpi" || die "download failed: $SRC_URL"
head -c2 "$TMP/sbx.xpi" | grep -q PK || die "not a valid .xpi (zip) — wrong host/URL?"
mkdir -p "$TMP/ext" && ( cd "$TMP/ext" && unzip -q "$TMP/sbx.xpi" )
SRCDIR="$TMP/ext"
fi
say "Launching Firefox with the ToolBoX extension loaded (temporary)…"
FXBIN="${FX%% *}" # web-ext wants the binary, not a flatpak wrapper
if [ "$FX" = "${FX# }" ] && command -v "$FXBIN" >/dev/null 2>&1; then
exec runwebext run --source-dir "$SRCDIR" --firefox "$FXBIN" \
--start-url "https://${HOST}/social/me"
fi
exec runwebext run --source-dir "$SRCDIR" --start-url "https://${HOST}/social/me"
fi
# ── fallback: open the .xpi so Firefox shows the install prompt ──
warn "web-ext not found (no npx) — falling back to the install prompt."
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
[ "$MODE" = "local" ] && die "--local needs web-ext/npx; install nodejs or use a host/--release"
say "Downloading ${SRC_URL}"
curl -fsSL "$SRC_URL" -o "$TMP/secubox-toolbox-webext.xpi" || die "download failed"
head -c2 "$TMP/secubox-toolbox-webext.xpi" | grep -q PK || die "not a valid .xpi"
cat <<'NOTE'
! The .xpi is unsigned. Stock Firefox release refuses a permanent install.
Use Firefox ESR/Developer/Nightly, or set in about:config:
xpinstall.signatures.required = false
…then accept the install prompt that opens now.
NOTE
say "Opening Firefox on the extension…"
exec $FX "$TMP/secubox-toolbox-webext.xpi"

View File

@ -1,36 +0,0 @@
{
"manifest_version": 3,
"name": "SecuBox ToolBoX — Cartographie sociale",
"version": "0.1.5",
"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": {
"gecko": {
"id": "secubox-toolbox-webext@cybermind.fr",
"strict_min_version": "115.0"
}
},
"permissions": ["storage", "alarms"],
"host_permissions": [
"*://*.secubox.in/*"
],
"action": {
"default_title": "SecuBox Cartographie",
"default_popup": "popup/popup.html",
"default_icon": {
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"icons": {
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"background": {
"service_worker": "background.js",
"scripts": ["api.js", "background.js"]
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": true
}
}

View File

@ -1,45 +0,0 @@
<!DOCTYPE html>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<html lang="fr">
<head>
<meta charset="utf-8">
<title>SecuBox Cartographie — Réglages</title>
<style>
body { background:#0a0a0f; color:#e8e6d9; font:14px/1.5 system-ui,sans-serif;
max-width:520px; margin:40px auto; padding:0 20px; }
h1 { color:#c9a84c; font-size:18px; }
label { display:block; color:#6b6b7a; font-size:12px; margin:14px 0 4px; }
input,select { width:100%; padding:8px; border-radius:6px; border:1px solid #333;
background:#14141c; color:#e8e6d9; }
button { margin-top:16px; padding:9px 14px; border-radius:6px; border:1px solid #c9a84c;
background:#c9a84c; color:#0a0a0f; font-weight:700; cursor:pointer; }
.muted { color:#6b6b7a; font-size:12px; }
#msg { color:#00ff41; min-height:18px; margin-top:10px; }
</style>
</head>
<body>
<h1>👁️ SecuBox Cartographie — Réglages</h1>
<p class="muted">L'extension parle uniquement à ta cabine via le tunnel R3.
Aucune donnée n'est envoyée ailleurs.</p>
<label>Borne (hôte de la cabine)
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
</label>
<label>Fenêtre d'analyse
<select id="since">
<option value="3600">1 heure</option>
<option value="86400" selected>24 heures</option>
<option value="604800">7 jours</option>
</select>
</label>
<label>Jeton de session (optionnel — sinon appairage auto via R3)
<input id="token" type="text" placeholder="mac.exp.nonce.sig" autocomplete="off">
</label>
<button id="save">Enregistrer</button>
<p id="msg"></p>
<script src="../api.js"></script>
<script src="options.js"></script>
</body>
</html>

View File

@ -1,24 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
const api = globalThis.SbxApi;
const $ = (id) => document.getElementById(id);
async function load() {
const cfg = await api.getConfig();
$("host").value = cfg.host;
$("token").value = cfg.token || "";
$("since").value = String(cfg.since);
}
$("save").addEventListener("click", async () => {
await api.setConfig({
host: $("host").value.trim() || api.DEFAULTS.host,
token: $("token").value.trim(),
since: parseInt($("since").value, 10) || api.DEFAULTS.since,
});
api.ext.runtime.sendMessage({ type: "refresh" });
$("msg").textContent = "Enregistré ✓";
setTimeout(() => ($("msg").textContent = ""), 1500);
});
load();

View File

@ -1,80 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// Dependency-free mini "Round-Eye" cartographie : the device at the
// centre, top trackers on an outer ring, radius/colour by hits + tier.
// A compact stand-in for the full d3 view served at /social/{token}.
const SVGNS = "http://www.w3.org/2000/svg";
const PAL = {
base: "#c9a84c", // gold
cdn: "#00d4ff", // cyan
ab: "#e63946", // cinnabar (anti-bot)
opg: "#6e40c9", // void purple (operator-grade)
eye: "#00ff41", // matrix green
link: "#2a2a3a",
};
function el(name, attrs) {
const n = document.createElementNS(SVGNS, name);
for (const k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function tierOf(node) {
if (node.opgrade_vendor) return "opg";
if (node.antibot_vendor) return "ab";
if (node.cdn_vendor) return "cdn";
return "base";
}
function renderGraph(svg, data) {
while (svg.firstChild) svg.removeChild(svg.firstChild);
const W = 260, H = 180, cx = W / 2, cy = H / 2;
const nodes = (data && data.nodes ? data.nodes.slice() : [])
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
.slice(0, 14);
if (!nodes.length) {
const t = el("text", { x: cx, y: cy, fill: "#6b6b7a", "font-size": 11,
"text-anchor": "middle" });
t.textContent = "Aucun traceur détecté pour l'instant";
svg.appendChild(t);
return;
}
const maxHits = Math.max(...nodes.map((n) => n.hits || 1));
const R = 66;
// spokes first (under the dots)
nodes.forEach((n, i) => {
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
svg.appendChild(el("line", { x1: cx, y1: cy, x2: x, y2: y,
stroke: PAL.link, "stroke-width": 1 }));
});
// tracker dots
nodes.forEach((n, i) => {
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
const r = 3 + Math.round(6 * Math.sqrt((n.hits || 1) / maxHits));
const fill = PAL[tierOf(n)];
const c = el("circle", { cx: x, cy: y, r, fill, "fill-opacity": 0.85 });
const title = el("title", {});
title.textContent = `${n.domain}${n.hits || 0} hits`
+ (n.cdn_vendor ? ` · ${n.cdn_vendor}` : "")
+ (n.antibot_vendor ? ` · anti-bot ${n.antibot_vendor}` : "")
+ (n.opgrade_vendor ? ` · opérateur ${n.opgrade_vendor}` : "");
c.appendChild(title);
svg.appendChild(c);
});
// the eye (device) at the centre
svg.appendChild(el("circle", { cx, cy, r: 13, fill: "#0c0c12",
stroke: PAL.eye, "stroke-width": 2 }));
svg.appendChild(el("circle", { cx, cy, r: 4.5, fill: PAL.eye }));
}
globalThis.renderGraph = renderGraph;

View File

@ -1,92 +0,0 @@
/* SPDX-License-Identifier: LicenseRef-CMSD-1.0 */
/* SecuBox cyberpunk/hermetic palette (DESIGN-CHARTER) */
:root {
--cosmos: #0a0a0f;
--gold: #c9a84c;
--cinnabar: #e63946;
--matrix: #00ff41;
--void: #6e40c9;
--cyan: #00d4ff;
--text: #e8e6d9;
--muted: #6b6b7a;
}
* { box-sizing: border-box; }
body {
width: 300px;
margin: 0;
background: var(--cosmos);
color: var(--text);
font: 13px/1.4 system-ui, "Segoe UI", sans-serif;
padding: 10px 12px 8px;
}
header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 8px;
}
.logo { color: var(--gold); font-weight: 700; letter-spacing: .5px; }
.r3 {
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
border: 1px solid currentColor;
}
.r3.on { color: var(--matrix); }
.r3.off { color: var(--muted); }
.muted { color: var(--muted); font-size: 11px; }
.err { color: var(--cinnabar); font-size: 11px; min-height: 14px; }
label { display: block; font-size: 11px; color: var(--muted); margin: 8px 0 4px; }
input[type=text] {
width: 100%; padding: 7px 8px; border-radius: 6px;
border: 1px solid #333; background: #14141c; color: var(--text);
}
button {
cursor: pointer; border: 1px solid #333; border-radius: 6px;
background: #14141c; color: var(--text); padding: 7px 8px; font-size: 12px;
}
button:hover { border-color: var(--gold); }
button.go {
width: 100%; margin-top: 8px; background: var(--gold); color: var(--cosmos);
font-weight: 700; border-color: var(--gold);
}
button.danger { color: var(--cinnabar); border-color: var(--cinnabar); }
.stats { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; margin-bottom: 8px; }
.stat {
background: #12121a; border: 1px solid #222; border-radius: 6px;
padding: 6px 2px; text-align: center;
}
.stat b { display: block; font-size: 16px; color: var(--gold); }
.stat span { font-size: 9px; color: var(--muted); }
.stat.warn b { color: var(--cinnabar); }
.stat.opg b { color: var(--void); }
#graph { width: 100%; height: 180px; background: #0c0c12; border-radius: 8px; display: block; }
.toplist { margin: 8px 0; max-height: 132px; overflow-y: auto; }
.row {
display: flex; align-items: center; gap: 6px; padding: 3px 2px;
border-bottom: 1px solid #1a1a22; font-size: 11px;
}
.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 .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.cdn { background: #1d2a33; color: var(--cyan); }
.tier.ab { background: #2a1416; color: var(--cinnabar); }
.tier.opg { background: #1e1430; color: var(--void); }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 6px 0; }
.actions button:last-child { grid-column: 1 / 3; }
footer {
display: flex; justify-content: space-between; align-items: center;
margin-top: 6px; padding-top: 6px; border-top: 1px solid #1a1a22;
}
footer a { color: var(--cyan); text-decoration: none; font-size: 11px; }

View File

@ -1,68 +0,0 @@
<!DOCTYPE html>
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="popup.css">
<title>SecuBox Cartographie</title>
</head>
<body>
<header>
<span class="logo">👁️ VILLAGE3B</span>
<span id="tordot" class="r3 off" title="Mode Tor" style="display:none">🧅</span>
<span id="r3dot" class="r3 off" title="État du tunnel R3">R3</span>
</header>
<!-- Pairing (shown when no token) -->
<section id="pair" hidden>
<p class="muted">Connecte-toi à la cabine pour voir qui t'observe.</p>
<label>Borne
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
</label>
<button id="pairBtn" class="go">Appairer (R3)</button>
<p id="pairMsg" class="err"></p>
</section>
<!-- Live view (shown when paired) -->
<section id="live" hidden>
<div class="stats">
<div class="stat"><b id="sTrackers"></b><span>traceurs</span></div>
<div class="stat"><b id="sSites"></b><span>sites</span></div>
<div class="stat warn"><b id="sAntibot"></b><span>anti-bot</span></div>
<div class="stat opg"><b id="sOpgrade"></b><span>opérateur</span></div>
</div>
<svg id="graph" viewBox="0 0 260 180" role="img" aria-label="Mini cartographie"></svg>
<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">
<button id="openFull">🗺️ Cartographie complète</button>
<button id="pdf">📄 Rapport PDF</button>
<button id="wipe" class="danger">🧹 Effacer mes données</button>
</div>
<p id="liveMsg" class="muted"></p>
</section>
<footer>
<a href="#" id="settings">Réglages</a>
<span class="muted" id="ver"></span>
</footer>
<script src="../api.js"></script>
<script src="graph.js"></script>
<script src="popup.js"></script>
</body>
</html>

View File

@ -1,194 +0,0 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
//
// SecuBox-Deb :: webext-toolbox :: popup controller
// NB: api.js (loaded first in this page) already declares `const ext` in the
// shared script scope — re-declaring it here is a "redeclaration of const ext"
// SyntaxError that aborts popup.js. Use api.ext instead.
const api = globalThis.SbxApi;
const $ = (id) => document.getElementById(id);
let curHost = api.DEFAULTS.host; // for favicon URLs (#555)
function show(which) {
$("pair").hidden = which !== "pair";
$("live").hidden = which !== "live";
}
function fillTopList(nodes) {
const list = $("topList");
list.innerHTML = "";
(nodes || [])
.slice()
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
.slice(0, 5)
.forEach((n) => {
const row = document.createElement("div");
row.className = "row";
// favicon of the major site/tracker (cabine proxy) — not an IP (#555)
const fav = document.createElement("img");
fav.className = "fav";
fav.loading = "lazy";
fav.alt = "";
fav.src = api.faviconUrl(curHost, n.domain || n.id);
fav.addEventListener("error", () => { fav.style.visibility = "hidden"; });
row.appendChild(fav);
const dom = document.createElement("span");
dom.className = "dom";
dom.textContent = n.domain || n.id;
row.appendChild(dom);
if (n.opgrade_vendor) addTier(row, "opg", n.opgrade_vendor);
else if (n.antibot_vendor) addTier(row, "ab", n.antibot_vendor);
else if (n.cdn_vendor) addTier(row, "cdn", n.cdn_vendor);
const hits = document.createElement("span");
hits.className = "hits";
hits.textContent = (n.hits || 0) + "×";
row.appendChild(hits);
list.appendChild(row);
});
}
function addTier(row, cls, label) {
const t = document.createElement("span");
t.className = "tier " + cls;
t.textContent = label;
row.appendChild(t);
}
function paint(data) {
const s = data.stats || {};
$("sTrackers").textContent = s.total_trackers ?? 0;
$("sSites").textContent = s.total_sites ?? 0;
$("sAntibot").textContent = s.antibot_sites ?? 0;
$("sOpgrade").textContent = s.opgrade_sites ?? 0;
globalThis.renderGraph($("graph"), data);
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() {
const cfg = await api.getConfig();
curHost = cfg.host || api.DEFAULTS.host;
$("ver").textContent = "v" + (api.ext.runtime.getManifest().version || "");
// tunnel indicator
api.r3Check(cfg.host).then((r) => {
const dot = $("r3dot");
dot.className = "r3 " + (r.tunnel ? "on" : "off");
dot.title = r.tunnel ? `Tunnel R3 actif (${r.peer_ip || "?"})` : "Hors tunnel R3";
});
// #683 — Tor egress indicator (only visible when kbin Tor mode is on)
api.torStatus(cfg.host).then((t) => {
const dot = $("tordot");
if (!dot) return;
if (t && t.tor_mode) {
dot.style.display = "";
dot.className = "r3 " + (t.running ? "on" : "off");
dot.title = t.running
? `Mode Tor actif — sortie anonymisée${t.exit_ip ? " (" + t.exit_ip + ")" : ""}`
: "Mode Tor activé — démarrage du tunnel…";
} else {
dot.style.display = "none";
}
});
if (!cfg.token) {
$("host").value = cfg.host;
show("pair");
return;
}
show("live");
$("liveMsg").textContent = "Chargement…";
try {
const data = await api.graph(cfg.host, cfg.token, cfg.since);
paint(data);
$("liveMsg").textContent = "";
loadProtection();
} catch (e) {
if (String(e.message) === "token-expired") {
// token died — drop it and go back to pairing
await api.setConfig({ token: "" });
show("pair");
$("host").value = cfg.host;
$("pairMsg").textContent = "Session expirée — ré-appaire.";
} else {
$("liveMsg").textContent = "Erreur : " + e.message;
}
}
}
// ── events ──
$("pairBtn").addEventListener("click", async () => {
const host = $("host").value.trim() || api.DEFAULTS.host;
$("pairMsg").textContent = "Appairage…";
try {
await api.setConfig({ host });
const token = await api.pair(host);
await api.setConfig({ token });
api.ext.runtime.sendMessage({ type: "refresh" });
await load();
} catch (e) {
$("pairMsg").textContent = e.message + " (es-tu sur le tunnel ?)";
}
});
$("openFull").addEventListener("click", async () => {
const cfg = await api.getConfig();
api.ext.tabs.create({ url: api.socialUrl(cfg.host, cfg.token) });
});
$("pdf").addEventListener("click", async () => {
const cfg = await api.getConfig();
api.ext.tabs.create({ url: api.reportUrl(cfg.host, cfg.token) });
});
$("wipe").addEventListener("click", async () => {
if (!confirm("Effacer toutes tes données de cartographie sur la cabine ?")) return;
const cfg = await api.getConfig();
try {
const r = await api.wipe(cfg.host, cfg.token);
$("liveMsg").textContent = `Effacé : ${r.rows_deleted ?? 0} entrées.`;
await api.setConfig({ token: "" });
setTimeout(load, 800);
} catch (e) {
$("liveMsg").textContent = "Erreur effacement : " + e.message;
}
});
$("settings").addEventListener("click", (e) => {
e.preventDefault();
api.ext.runtime.openOptionsPage();
});
load();

View File

@ -55,13 +55,4 @@ server {
proxy_pass http://unix:/run/secubox/system.sock:/; proxy_pass http://unix:/run/secubox/system.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf; include /etc/nginx/snippets/secubox-proxy.conf;
} }
# #65: per-module routes self-register here. Every module package drops a
# /etc/nginx/secubox-routes.d/<module>.conf (location-only snippet) at
# install time, so a newly added module's /<module>/ + /api/v1/<module>/
# routes are picked up automatically — no more hand-editing this file per
# module. This is the ACTIVE include (matches the deployed webui.conf).
# The crowdsec/waf/system blocks above stay hardcoded: those core packages
# only ship the legacy secubox.d/ snippet, so they would NOT duplicate here.
include /etc/nginx/secubox-routes.d/*.conf;
} }

View File

@ -1,10 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""Shared classifiers used by mitm-ingest enrich_hooks across modules.
- host_app : host/SNI app + category + emoji
- cookie : cookie name provider + category + emoji
- avatar : UA device + browser + os + emoji
- ja4 : TLS ClientHello fingerprint hash
"""
from . import host_app, cookie, avatar, ja4 # noqa: F401

View File

@ -1,116 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""Avatar analysis : UA + Client Hints → device emoji + readable name."""
from __future__ import annotations
import re
# Devices identification patterns. Order = priority (first match wins).
DEVICE_PATTERNS = [
# ── iPhone ──
(re.compile(r"iPhone\s?OS\s?(\d+_\d+)|iPhone.*OS\s?(\d+_\d+)", re.I),
"iPhone", "📱", "iPhone iOS {}"),
(re.compile(r"iPhone", re.I), "iPhone", "📱", "iPhone"),
# ── iPad ──
(re.compile(r"iPad", re.I), "iPad", "📱", "iPad"),
# ── Mac ──
(re.compile(r"Mac OS X (\d+[._]\d+)", re.I), "Mac", "💻", "macOS {}"),
(re.compile(r"Macintosh", re.I), "Mac", "💻", "Mac"),
# ── Android ──
(re.compile(r"Pixel\s?(\d+)", re.I), "Pixel", "📱", "Pixel {}"),
(re.compile(r"SM-[A-Z]\d+", re.I), "Samsung", "📱", "Samsung"),
(re.compile(r"Android (\d+)", re.I), "Android", "📱", "Android {}"),
(re.compile(r"Android", re.I), "Android", "📱", "Android"),
# ── Windows ──
(re.compile(r"Windows NT 11"), "Windows", "💻", "Windows 11"),
(re.compile(r"Windows NT 10"), "Windows", "💻", "Windows 10"),
(re.compile(r"Windows NT"), "Windows", "💻", "Windows"),
# ── Linux ──
(re.compile(r"Linux", re.I), "Linux", "🐧", "Linux"),
# ── Game / IoT ──
(re.compile(r"PlayStation", re.I), "PlayStation", "🎮", "PlayStation"),
(re.compile(r"Xbox", re.I), "Xbox", "🎮", "Xbox"),
(re.compile(r"Nintendo", re.I), "Nintendo", "🎮", "Nintendo"),
(re.compile(r"AppleTV", re.I), "Apple TV", "📺", "Apple TV"),
(re.compile(r"Roku", re.I), "Roku", "📺", "Roku"),
# ── Bot / known clients ──
(re.compile(r"curl/", re.I), "curl", "🛠", "curl"),
(re.compile(r"wget/", re.I), "wget", "🛠", "wget"),
]
BROWSER_PATTERNS = [
(re.compile(r"Edg/(\d+)"), "Edge", "🪟", "Edge {}"),
(re.compile(r"Chrome/(\d+)"), "Chrome", "🟢", "Chrome {}"),
(re.compile(r"Firefox/(\d+)"), "Firefox","🦊", "Firefox {}"),
(re.compile(r"Safari/(\d+)"), "Safari", "🧭", "Safari"),
(re.compile(r"OPR/(\d+)|Opera/(\d+)"), "Opera", "🔴", "Opera"),
(re.compile(r"DuckDuckGo/(\d+)"), "DuckDuckGo", "🦆", "DuckDuckGo {}"),
]
def classify_user_agent(ua: str) -> dict:
"""Returns {device, device_emoji, os_label, browser, browser_emoji, browser_label, raw}."""
if not ua:
return {"device": "unknown", "device_emoji": "", "os_label": "?",
"browser": "unknown", "browser_emoji": "", "browser_label": "?",
"raw": ""}
device_match = None
device_label = "unknown"
for pattern, label, emoji, template in DEVICE_PATTERNS:
m = pattern.search(ua)
if m:
# Try to fill the template with first non-None group
groups = [g for g in m.groups() if g]
if groups and "{}" in template:
device_label = template.format(groups[0].replace("_", "."))
else:
device_label = template
device_match = {"device": label, "device_emoji": emoji,
"os_label": device_label}
break
if not device_match:
device_match = {"device": "unknown", "device_emoji": "",
"os_label": ua[:50]}
browser_match = None
for pattern, label, emoji, template in BROWSER_PATTERNS:
m = pattern.search(ua)
if m:
groups = [g for g in m.groups() if g]
if groups and "{}" in template:
bl = template.format(groups[0])
else:
bl = template
browser_match = {"browser": label, "browser_emoji": emoji, "browser_label": bl}
break
if not browser_match:
browser_match = {"browser": "unknown", "browser_emoji": "", "browser_label": "?"}
return {**device_match, **browser_match, "raw": ua[:200]}
def analyze_user_agents(ua_set: set[str] | list[str]) -> dict:
"""Aggregate a set of UAs : returns {devices, browsers, most_common, raw_count}."""
if not ua_set:
return {"devices": {}, "browsers": {}, "most_common": None, "raw_count": 0}
devices: dict[str, dict] = {}
browsers: dict[str, dict] = {}
for ua in ua_set:
cls = classify_user_agent(ua)
d = cls["device"]
if d not in devices:
devices[d] = {"count": 0, "emoji": cls["device_emoji"], "os_label": cls["os_label"]}
devices[d]["count"] += 1
b = cls["browser"]
if b not in browsers:
browsers[b] = {"count": 0, "emoji": cls["browser_emoji"], "label": cls["browser_label"]}
browsers[b]["count"] += 1
# Most common device
most_common = max(devices.items(), key=lambda x: x[1]["count"])[0] if devices else None
return {
"devices": devices,
"browsers": browsers,
"most_common": most_common,
"most_common_emoji": devices[most_common]["emoji"] if most_common else "",
"raw_count": len(ua_set),
}

View File

@ -1,140 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""Cookie analysis : identify trackers + providers + categorize.
Phase 2a+ heuristic: pattern matching sur les noms de cookies bien connus,
mapping vers fournisseur + catégorie (analytics / advertising / social / etc.).
Database extensible pour Phase 3 on chargera depuis cookiepedia ou EasyList.
"""
from __future__ import annotations
import re
# Pattern → (provider, category, emoji)
COOKIE_PATTERNS = [
# ── Analytics ──
(re.compile(r"^_ga(_|$|t)"), "Google Analytics", "analytics", "📊"),
(re.compile(r"^_gid$"), "Google Analytics", "analytics", "📊"),
(re.compile(r"^_gat"), "Google Analytics", "analytics", "📊"),
(re.compile(r"^_gcl_au$"), "Google Ads conversion", "advertising", "💰"),
(re.compile(r"^_pk_(id|ses|cvar)"), "Matomo / Piwik", "analytics", "📊"),
(re.compile(r"^plausible_"), "Plausible", "analytics", "📊"),
(re.compile(r"^_mkto_trk$"), "Marketo", "analytics", "📊"),
(re.compile(r"^__hssc$|^__hstc$"), "HubSpot", "analytics", "📊"),
(re.compile(r"^mp_[a-z0-9]+_mixpanel"), "Mixpanel", "analytics", "📊"),
(re.compile(r"^amplitude_"), "Amplitude", "analytics", "📊"),
(re.compile(r"^optimizelyEndUserId$"), "Optimizely", "analytics", "📊"),
(re.compile(r"^_hjSession"), "Hotjar", "analytics", "📊"),
(re.compile(r"^_hjFirstSeen$"), "Hotjar", "analytics", "📊"),
(re.compile(r"^crisp-client/session/"), "Crisp Chat", "analytics", "💬"),
# ── Advertising / Tracking ──
(re.compile(r"^_fbp$|^fr$"), "Facebook Pixel", "advertising","🎯"),
(re.compile(r"^IDE$"), "Google DoubleClick", "advertising","🎯"),
(re.compile(r"^NID$"), "Google", "advertising","🎯"),
(re.compile(r"^DSID$"), "Google DoubleClick", "advertising","🎯"),
(re.compile(r"^uid$|^bcookie$|^lidc$"), "LinkedIn Insight", "advertising","💼"),
(re.compile(r"^MUID$|^_uetsid$|^_uetvid$"), "Microsoft Clarity / Bing Ads", "advertising", "🎯"),
(re.compile(r"^_pin_unauth$|^_pinterest_ct_"), "Pinterest", "advertising","📌"),
(re.compile(r"^tt_appInfo$|^tt_webid"), "TikTok", "advertising","🎵"),
(re.compile(r"^_ttp$"), "TikTok Pixel", "advertising","🎵"),
(re.compile(r"^ANID$"), "Google", "advertising","🎯"),
(re.compile(r"^__qca$"), "Quantcast", "advertising","🎯"),
(re.compile(r"^__gads$|^__gpi$"), "Google AdSense", "advertising","💰"),
(re.compile(r"^test_cookie$"), "Google", "advertising","🎯"),
# ── Social ──
(re.compile(r"^c_user$|^xs$|^datr$"), "Facebook", "social", "👥"),
(re.compile(r"^sb$|^locale$|^wd$"), "Facebook", "social", "👥"),
(re.compile(r"^twid$|^ct0$|^auth_token$"), "Twitter / X", "social", "👥"),
(re.compile(r"^li_at$"), "LinkedIn", "social", "👥"),
(re.compile(r"^IG_"), "Instagram", "social", "👥"),
# ── Auth / Session (legit, no tracker) ──
(re.compile(r"^session(_id)?$|^sessionid$"), "Session generic", "session", "🔑"),
(re.compile(r"^csrftoken$|^_csrf$"), "CSRF token", "session", "🔒"),
(re.compile(r"^XSRF-TOKEN$"), "XSRF token", "session", "🔒"),
(re.compile(r"^remember_token$"), "Remember-me", "session", "🔑"),
(re.compile(r"^PHPSESSID$"), "PHP session", "session", "🔑"),
(re.compile(r"^JSESSIONID$"), "Java session", "session", "🔑"),
(re.compile(r"^connect\.sid$"), "Express.js session", "session", "🔑"),
# ── CDN / infra ──
(re.compile(r"^__cf_bm$|^cf_clearance$"), "Cloudflare", "infra", ""),
(re.compile(r"^_dd_s$"), "Datadog RUM", "monitoring", "📈"),
]
def classify_cookie_name(name: str) -> dict:
"""Returns {provider, category, emoji} for a single cookie name.
Unknown {provider: 'unknown', category: 'other', emoji: ''}."""
for pattern, provider, category, emoji in COOKIE_PATTERNS:
if pattern.search(name):
return {"provider": provider, "category": category, "emoji": emoji}
return {"provider": "unknown", "category": "other", "emoji": ""}
def parse_cookie_header(header_value: str) -> list[str]:
"""Parse 'Cookie:' or 'Set-Cookie:' value, return list of cookie NAMES."""
if not header_value:
return []
names = []
for part in header_value.split(";"):
if "=" in part:
n = part.split("=", 1)[0].strip()
if n:
names.append(n)
return names
def analyze_cookie_events(cookie_events: list[dict]) -> dict:
"""Aggregate cookie events into stats + per-provider breakdown.
Input : list of {url, set_cookie_count, cookie_count, ...} from local_store
(note : Phase 1.5 stored only counts, not names. Phase 2a+ local_store
should store names. Until then, this function works on whatever's present.)
Returns :
{
providers: {provider: {count, category, emoji}, ...},
categories: {category: count, ...},
unknown_count: int,
}
"""
providers: dict[str, dict] = {}
categories: dict[str, int] = {}
unknown_count = 0
for ev in cookie_events:
# The cookie name might be in `set_cookie_names` or `cookie_names` if Phase 2a+
# local_store. Backward-compat : skip if absent.
for key in ("set_cookie_names", "cookie_names"):
names = ev.get(key, [])
if not isinstance(names, list):
continue
for n in names:
cls = classify_cookie_name(n)
p = cls["provider"]
if p == "unknown":
unknown_count += 1
else:
if p not in providers:
providers[p] = {"count": 0, "category": cls["category"],
"emoji": cls["emoji"]}
providers[p]["count"] += 1
cat = cls["category"]
categories[cat] = categories.get(cat, 0) + 1
return {
"providers": providers,
"categories": categories,
"unknown_count": unknown_count,
}
# Quick lookup for live use in /report endpoints
def top_providers(cookie_events: list[dict], limit: int = 10) -> list[dict]:
"""Returns top providers by hit count : [{provider, count, category, emoji}, ...]"""
stats = analyze_cookie_events(cookie_events)
return sorted(
[{"provider": p, **v} for p, v in stats["providers"].items()],
key=lambda x: -x["count"],
)[:limit]

View File

@ -1,84 +0,0 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""JA4 / JA4-like TLS ClientHello fingerprint.
Reference: https://github.com/FoxIO-LLC/ja4 (BSD-3)
Phase 2c implementation : compute a deterministic, JA4-style fingerprint
hash from cipher_suites + alpn_protocols + extensions. The output is
12-char hex (truncated SHA256), suitable for matching against external
JA4 databases (custom curation, not the full FoxIO format).
This is NOT the canonical FoxIO JA4 string. It's a deterministic
fingerprint that's stable per-client-stack, so the same iPhone Safari
will always yield the same hash. We can map known hashes to bots,
trackers, malware C2 in Phase 3.
"""
from __future__ import annotations
import hashlib
def _sort_norm(items: list | None) -> str:
"""Sort + join items as canonical comma-separated lowercase string."""
if not items:
return ""
parts = []
for x in items:
if isinstance(x, bytes):
parts.append(x.hex())
else:
parts.append(str(x).lower())
return ",".join(sorted(parts))
def compute_ja4_hash(
*,
sni: str | None = None,
alpn_protocols: list | None = None,
cipher_suites: list | None = None,
extensions: list | None = None,
transport: str = "t", # 't' for TCP, 'q' for QUIC
tls_version: str = "13", # 13 for TLS 1.3, 12 for TLS 1.2
) -> dict:
"""Compute a JA4-style fingerprint dict.
Returns {
fingerprint : 12-char hex hash,
transport : t/q,
tls_version : 13/12,
alpn_count : int,
cipher_count : int,
ext_count : int,
sni_present : bool,
raw_repr : compact str repr for debug,
}
"""
alpn_str = _sort_norm(alpn_protocols)
cipher_str = _sort_norm(cipher_suites)
ext_str = _sort_norm(extensions)
raw = f"{transport}{tls_version}|alpn={alpn_str}|c={cipher_str}|x={ext_str}"
h = hashlib.sha256(raw.encode("utf-8", errors="ignore")).hexdigest()[:12]
return {
"fingerprint": h,
"transport": transport,
"tls_version": tls_version,
"alpn_count": len(alpn_protocols or []),
"cipher_count": len(cipher_suites or []),
"ext_count": len(extensions or []),
"sni_present": bool(sni),
"raw_repr": raw[:200],
}
# Phase 3-ready : map known JA4 hashes to client tags. Empty for now.
KNOWN_JA4_FINGERPRINTS: dict[str, dict] = {
# "abc123def456": {"label": "iPhone Safari 17.x", "category": "browser", "trust": "high"},
# "deadbeef0000": {"label": "Tor Browser 14.x", "category": "browser-anon", "trust": "medium"},
}
def lookup_ja4(fingerprint: str) -> dict | None:
"""Return known label for a fingerprint, or None if unknown."""
return KNOWN_JA4_FINGERPRINTS.get(fingerprint)

View File

@ -1,107 +0,0 @@
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
# AI Handover — prompt Mistral.ai (reprise du code + analyse projet)
Prompt prêt à coller dans **Mistral Le Chat** (ou via l'API) pour qu'un agent
reprenne le code SecuBox-Deb et analyse le projet.
**Usage :** Le Chat n'a pas accès au dépôt ni au board `gk2` par défaut. Pour une
vraie reprise, lance l'agent dans un IDE/agent ayant accès au filesystem + SSH,
ou colle-lui `CLAUDE.md` + `.claude/*` en contexte. Mets à jour la section
« ÉTAT ACTUEL » depuis `.claude/HISTORY.md` avant chaque réutilisation.
---
```
# RÔLE
Tu es un ingénieur senior Debian / Python / sécurité réseau qui REPREND le projet
SecuBox-Deb. Tu travailles méthodiquement : tu LIS avant d'écrire, tu vérifies
avant d'affirmer, tu respectes à la lettre les conventions ci-dessous, et tu
n'inventes pas de fichiers/commandes — tu les vérifies dans le dépôt. Langue : français.
# CONTEXTE PROJET
SecuBox-Deb = plateforme cybersécurité CyberMind, portage Debian 12 (Bookworm)
ARM64 depuis OpenWrt, cible ANSSI CSPN. Matériel : MOCHAbin / ESPRESSObin
(Marvell Armada, aarch64). Dev : Gérald Kerma (Gandalf). Dépôt :
github.com/CyberMind-FR/secubox-deb.
Stack : Debian bookworm, kernel 6.x, nftables (PAS iptables), Unbound (Vortex DNS),
HAProxy + mitmproxy (WAF), Suricata + CrowdSec, FastAPI/Uvicorn (sockets unix par
module), LXC (pas Docker pour les apps), WireGuard, SQLite par défaut.
Palette cyberpunk/hermétique : cosmos #0a0a0f, gold #c9a84c, cinnabar #e63946,
matrix #00ff41, void #6e40c9, cyan #00d4ff. Polices Cinzel / IM Fell / JetBrains Mono.
# À LIRE EN PREMIER (sources de vérité)
1. CLAUDE.md + .claude/CLAUDE.md — règles impératives.
2. .claude/WIP.md — travail en cours + « Next Up ».
3. .claude/HISTORY.md — historique daté (commence par l'entrée la plus récente).
4. .claude/PATTERNS.md, .claude/MODULE-COMPLIANCE.md, .claude/MIGRATION-MAP.md.
5. docs/TOOLS.md, scripts/README.md.
# RÈGLES IMPÉRATIVES (non négociables)
- nftables DEFAULT DROP ; jamais iptables ni uci/LuCI.
- JAMAIS de waf_bypass : tout le trafic passe par mitmproxy.
- Secrets hors code : /etc/secubox/secrets/ chmod 600 ; jamais en clair / en TOML versionné.
- En-tête SPDX LicenseRef-CMSD-1.0 sur chaque fichier (vérifié par scripts/license-headers.py --check).
- SQLite par défaut (pas MySQL/Postgres sauf exception documentée).
- AppArmor enforce + user dédié secubox-<module> par service.
- Packaging Architecture:all pour le Python ; debian/compat=13, Standards-Version 4.6.2.
override_dh_strip est MORT pour Architecture:all → installer via execute_after_dh_auto_install.
- Pas de référence « Claude Code » / outil IA dans les commits/PR.
# WORKFLOW (multi-agent worktree)
- Tout travail non trivial = worktree dédié : bash scripts/agent-worktree.sh start --issue <#>
(branche feature/<#>-… ou fix/<#>-… selon le label ; master réservé au housekeeping).
- Cycle : issue GitHub → worktree → commits « (ref #<#>) » → PR « Closes #<#> » →
merge → agent-worktree.sh clean <#>. Ne jamais fermer une issue automatiquement.
- Build .deb : cd packages/<pkg> && dpkg-buildpackage -us -uc -b -d (le -d ok pour arch:all).
# DÉPLOIEMENT LIVE (board « gk2 »)
- SSH : root@192.168.1.200 (LAN) ou root@10.98.0.1 (tunnel wg-admin) ; clé en place.
- Portail toolbox = secubox-toolbox.service (host, uvicorn secubox_toolbox.app:app
sur 0.0.0.0:8088). HAProxy : kbin.gk2.secubox.in → backend toolbox_landing → 10.99.0.1:8088.
- R3 = 4 workers host-native secubox-toolbox-mitm-wg-worker@{1..4}.service
(mitmdump 10.99.1.1:8081-8084) chargeant les addons depuis
/usr/lib/secubox/toolbox/mitmproxy_addons/ (liste dans sbin/secubox-toolbox-mitm-wg-launch).
- Recette deploy : build → scp .deb → dpkg -i --force-confold --force-confdef →
TOUJOURS vérifier portail actif ET curl -sk https://kbin.gk2.secubox.in/ == 200
(un upgrade SIGTERM le portail ; le postinst le relance depuis 2.6.29, mais vérifie).
Changement d'addon → redémarrer les 4 workers SÉQUENTIELLEMENT (RAM limitée).
Ne PAS faire de restart de masse secubox-* (~100+ daemons).
# ARCHITECTURE TOOLBOX (module le plus actif)
packages/secubox-toolbox/ : FastAPI (secubox_toolbox/api.py, app.py), addons
mitmproxy (mitmproxy_addons/), filtres modulaires (secubox_toolbox/filters.py →
/etc/secubox/toolbox/filters.json, togglés via /admin/filters/ui). Store social :
SQLite /var/lib/secubox/toolbox/toolbox.db (social_edges/nodes/links/host_meta/
antibot/opgrade + threat_intel). Cartographie : www/toolbox/social.js (vues donut /
domaines-nuggets / œil), index.html (WebUI 5 onglets). Addons : inject_banner,
protective_mode, ad_ghost, media_cache, media_stats, social_graph, dpi, cookies,
avatar, ja4, utiq_defense, cert_pin_detect. Niveaux clients : R0/R1 (sans
bannière), R2 (captif), R3 (tunnel WG 10.99.1.0/24), R4 (prévu).
# ÉTAT ACTUEL (2026-06-14 — RAFRAÎCHIR depuis HISTORY avant réutilisation)
secubox-toolbox 2.6.36 déployé live, kbin sain. Live : protective spoofer,
filtres modulaires + ad-ghoster (collapse), media cache (opt-in), autolearn
trackers, DPI media donut, cartographie donut + nuggets domaine (IPs cachées) +
favicons, bannière guirlande + pin partagé, panneau protection webext,
/ca/fingerprint R3, fix postinst (kbin 503), detect_antibot deployment-vs-challenge.
Clients : APK Android v0.3.0 (zero-tap), webext v0.1.4. Fix : sync photos
iPhone↔Nextcloud (files_antivirus off + limites PHP).
# TRAVAIL OUVERT
#592 secubox-webmail-hub : inbox unifié Gmail (OAuth2) + Gandi + OVH ssl0, toutes
les sous-boîtes/alias en une page. Design filé, BLOQUÉ : besoin d'un client OAuth
Google (client_id/secret/redirect) + nom de vhost + décision read-only. Phase 1
IMAP (Gandi/OVH) peut démarrer sans OAuth.
# TES PREMIÈRES TÂCHES
1. ANALYSE (sans rien modifier) : lis .claude/* + CLAUDE.md, puis produis une
synthèse structurée — architecture, état des modules (✅/🔄/⬜ via
MIGRATION-MAP.md), dette technique, risques sécurité, écarts CSPN, backlog
priorisé. Cite chemin:ligne.
2. Propose un plan pour l'item « Next Up » (ou #592), conforme au workflow worktree
+ aux règles, AVANT d'écrire du code.
3. Toute action sur le board live : décris-la et demande confirmation si difficile
à annuler ou exposée.
Commence par : « J'ai lu CLAUDE.md, .claude/WIP.md et HISTORY.md. Voici ma synthèse… »
```

View File

@ -1,93 +0,0 @@
# FAQ — kbin & le mode Tor anonymisé
> kbin (`kbin.gk2.secubox.in`) = le portail public de la **ToolBoX** SecuBox, premier
> outil du couteau suisse cyber CyberMind. Cette FAQ couvre le surf protégé et le futur
> **mode Tor quick-switch** ([#683](https://github.com/CyberMind-FR/secubox-deb/issues/683)).
---
### Qu'est-ce que kbin exactement ?
Le portail public de `secubox-toolbox`. On rejoint l'AP libre de la cabine, on consent,
et tout le trafic traverse le pipeline de forge MITM SecuBox : inspection chiffrée,
nettoyage pub/tracker, bandeau de transparence, safe browsing. Voir
[Kbin-Toolbox](wiki/Kbin-Toolbox.md).
### kbin voit-il tout mon trafic ? C'est pas dangereux ?
C'est **consenti et éphémère**. La MAC est hashée avec un sel rotatif 24 h, aucune valeur
de cookie brute n'est persistée, aucun mapping session ↔ identité réelle ne survit au TTL.
Trois niveaux d'opt-in : R0 (bypass complet), R1 (analyse passive, recommandé), R2/R3
(TLS-break + bandeau). Sans consentement, **pas** de déchiffrement.
### « Performance transparente », ça veut dire quoi ?
On ne déchiffre que ce qu'on modifie. Les flux pur-asset (vidéo, images CDN) sont
*splicés* dès le ClientHello TLS (`tls_splice`, #649) — les workers ne forgent/déchiffrent
pas ce qui n'a aucune valeur L7. Débit ligne, latence quasi nulle.
### C'est quoi « l'injection de poison et de smog » ?
Le trafic ad-tech et tracker n'est pas seulement bloqué : il est **empoisonné**. Anti-Track
v2 (#633) renvoie des pseudo-réponses, neutralise les scripts CDN préchargés, et au niveau
réseau fait de l'IP-drop + DNS-refuse. Le profil publicitaire ressort pollué, pas vide —
indistinguable d'un vrai blocage côté tracker.
### Le bandeau anti-adware, il bloque quoi ?
Une bannière de transparence injectée dans la page : nombre de trackers vus/bloqués,
acteurs reconnus cross-site. Elle est immune au CSP et SPA-aware (#636/#639, webext #655).
C'est l'affichage ; le blocage réel vient des blocklists Vortex DNS + blacklist nft.
---
## Mode Tor (plan #683)
### Le mode Tor, ça fait quoi ?
Un interrupteur 🧅 sur kbin : un tap → ton surf ressort **par le réseau Tor** au lieu du
WAN de la box. IP de sortie anonyme, identité réseau masquée — du « pseudo-network
surfing ».
### Est-ce que kbin arrête de m'inspecter/protéger en mode Tor ?
Non. Tor se place **après** le cœur de forge MITM, sur le transport upstream (dialer
SOCKS5). Tu gardes le poison/smog, le bandeau et le safe browsing ; **seules l'IP de sortie
et l'identité réseau changent**.
### Et si Tor tombe, ça repasse en clair ?
**Jamais.** Le design est **fail-closed** : si Tor n'est pas disponible, le trafic est
coupé, pas renvoyé en clearnet. L'anonymat est un invariant, pas un best-effort.
### Y a-t-il des fuites DNS ?
Non. Quand le mode Tor est actif, la résolution passe **par Tor**, pas par l'Unbound local.
### C'est la même chose que `secubox-exposure` ?
Non, direction opposée. `secubox-exposure` publie des **services cachés** Tor (entrant —
exposer un service interne). kbin Tor endpoint fait sortir ton **surf** par Tor (sortant).
Le contrôle Tor (bootstrap, NEWNYM/nouvelle identité) est réutilisé entre les deux.
### Comment je change d'IP de sortie ?
Bouton « nouvelle identité » (NEWNYM) → nouveau circuit Tor → nouvelle IP de sortie, à la
volée, sans reconnecter.
### C'est activé par défaut ?
Non. **Opt-in par client** (scopé WG-hash), **défaut OFF**, respecte ton niveau de
consentement R. Chaque bascule on/off est journalisée (audit-log CSPN immuable).
---
## Voir aussi
- [Kbin-Toolbox](wiki/Kbin-Toolbox.md) — la page use-case complète
- [Spec mode Tor](superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md)
- [Anti-Track](wiki/Anti-Track.md) — bloque/empoisonne/anonymise (couche DNS/IP)
---
*CyberMind — Gérald Kerma · LicenseRef-CMSD-1.0*

View File

@ -1,147 +0,0 @@
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
# SecuBox-Deb — CSPN Test Matrix (draft)
Maps the ANSSI **CSPN** evaluation themes + the project's stated security
functions (CLAUDE.md §"Contraintes ANSSI CSPN") to **concrete, mostly
automatable tests**. Target home for the automated rows: `tests/cspn/`
(pytest, gated in CI). Each row is an *acceptance check* with a command/
assertion and the evidence artifact an evaluator would expect.
**Legend** — Type: `A`=automated (pytest/CI), `M`=manual/pentest, `D`=doc/spec.
Status: ⬜ todo · 🔄 partial · ✅ covered.
> Scope note: the **cible de sécurité** (security target) must be written
> first (TOE boundary, assumptions, threats, security functions). This
> matrix is the *robustness + conformity* test plan that hangs off it.
---
## 0. Security target & conformity (D)
| ID | Requirement | Type | Method / artifact | Pass | St |
|----|-------------|------|-------------------|------|----|
| ST-01 | Cible de sécurité rédigée (TOE, hypothèses, menaces, FS) | D | `docs/cspn/cible-securite.md` reviewed | doc complete + signed | ⬜ |
| ST-02 | TOE boundary & versions pinned | D | version manifest (pkg list + hashes) per release | matches APT repo | ⬜ |
| ST-03 | Conformity: spec ↔ impl traceability | D | each FS → code path + test ID | 100% FS mapped | ⬜ |
## 1. Cryptography — TLS / keys / RNG
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| CRY-01 | TLS 1.3 min; TLS ≤1.1 refused (HAProxy frontends) | A | `openssl s_client -tls1_1 -connect <vhost>:443` → handshake fail; `-tls1_3` → ok | 1.0/1.1/1.2-weak refused | ⬜ |
| CRY-02 | Strong cipher suites only (no RC4/3DES/CBC-legacy) | A | `nmap --script ssl-enum-ciphers` / testssl.sh grade ≥ A | A grade, no weak | ⬜ |
| CRY-03 | HSTS + secure headers on exposed vhosts | A | `curl -sI``Strict-Transport-Security`, `X-Content-Type-Options` | present | ⬜ |
| CRY-04 | Private keys 0600, owner-restricted, not world-readable | A | `stat -c %a` on `/etc/secubox/**/key.pem`, ACME keys | 600, non-root svc owner | 🔄 |
| CRY-05 | CA / mitm keys never in VCS or logs | A | `git grep -nE 'BEGIN (RSA |EC )?PRIVATE KEY'` == empty; journald scrub | no hits | ⬜ |
| CRY-06 | RNG source = kernel CSPRNG for tokens/keys | A | code audit: `secrets`/`os.urandom`, no `random` for security | no `random.` in sec paths | 🔄 |
| CRY-07 | mitm R3 CA fingerprint published & verifiable | A | `/ca/fingerprint?ca=wg` == cert on disk (sha256) | match (D5:E4:3A…) | ✅ |
## 2. Authentication & session
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| AUT-01 | All API endpoints require JWT (`Depends(require_jwt)`) | A | enumerate FastAPI routes; assert auth dep except allowlist | 100% gated | 🔄 |
| AUT-02 | Unauthenticated request → 401, no data leak | A | `curl` each `/api/v1/*` sans token | 401, empty body | ⬜ |
| AUT-03 | JWT signature verified; tampered/expired rejected | A | forge/expire token → 401 | rejected | ⬜ |
| AUT-04 | Social/report tokens = HMAC, TTL-bound, salt-rotated | A | expired/forged `/social/{token}` → 403; salt rotates daily | rejected + rotation | 🔄 |
| AUT-05 | No default/hardcoded credentials | A | grep configs + first-boot generates per-device secrets | none | 🔄 |
| AUT-06 | Brute-force handled at the WAF layer (per project doctrine) | M | rate-limit probe via HAProxy/CrowdSec | throttled/banned | 🔄 |
| AUT-07 | ZKP auth (GK-HAM-2025) NIZK soundness, G rotation 24h PFS | M+A | protocol test vectors + rotation timer check | proofs verify, rotates | ⬜ |
## 3. Access control / privilege separation
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| ACL-01 | Each daemon runs as `secubox-<module>` (not root) | A | `systemctl show -p User` over all `secubox-*` units | non-root each | 🔄 |
| ACL-02 | AppArmor profile present + **enforce** per service | A | `aa-status` lists each profile in enforce | all enforce | ⬜ |
| ACL-03 | systemd hardening (ProtectSystem, NoNewPrivileges, etc.) | A | `systemd-analyze security secubox-*` score | exposure ≤ medium | ⬜ |
| ACL-04 | Filesystem perms: `/etc/secubox/secrets` 0600, parents traversable but not writable | A | `stat` perms + traversal test as svc user | 0600 secrets, 0755 parents | 🔄 |
| ACL-05 | No unintended setuid/world-writable shipped | A | `find / -perm -4000 / -perm -0002` in image | known allowlist only | ⬜ |
## 4. Network filtering / attack surface
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| NET-01 | nftables policy DEFAULT DROP (input/forward) | A | `nft list chain inet filter input``policy drop` | drop | ✅ (verify) |
| NET-02 | Only declared ports open; no stray listeners | A | `ss -tlnp` ∩ documented port map | exact match | 🔄 |
| NET-03 | WAN-side SSH closed (key-only + source-restricted) | A | sshd `PasswordAuthentication no`; nft SSH-guard drops non-LAN/tunnel | enforced | ✅ |
| NET-04 | No IPv6 leak past the v4 firewall guards | A | nft inet covers v6; `ss` v6 listeners reviewed | covered | ⬜ |
| NET-05 | nft rules persist across reboot + survive pkg upgrade | A | reboot/upgrade → drop-ins reload; ruleset intact | persists | 🔄 |
| NET-06 | DNS = Unbound only; DoH/DoT exfil detected/blocked (opt-in) | A | resolve via Unbound; DoH probe flagged | controlled | 🔄 |
## 5. WAF / traffic inspection integrity (no bypass)
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| WAF-01 | No `waf_bypass` anywhere; all vhosts → mitm inspector | A | grep HAProxy cfg; each backend = mitmproxy_inspector (or documented exception) | no bypass | 🔄 |
| WAF-02 | mitm CA only trusted on consenting (R2/R3) clients | A | non-consenting client not MITM'd | scoped | ✅ |
| WAF-03 | Banner/transparency shown to inspected clients (CSPN R2 req) | A | inspected HTML carries the banner guard | present | ✅ |
| WAF-04 | Active interference (spoof/ghost) is opt-in + logged + reversible | A | filters default-safe; every action → audit.log; toggle off restores | conforms | ✅ |
| WAF-05 | mitm fail-open never serves attacker-controlled content | M | malformed upstream / addon exception → flow unbroken, no inject error | safe | 🔄 |
## 6. Logging & audit (immutability)
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| LOG-01 | Security decisions (ban/unban/spoof/escalate/rule-change) logged to `/var/log/secubox/audit.log` | A | trigger each → grep audit line | logged | 🔄 |
| LOG-02 | Timestamps RFC 3339 / ISO-8601 with TZ | A | regex each audit line | conforms | 🔄 |
| LOG-03 | Append-only / rotation without truncate (immutability) | A | `chattr +a` or rotate-copy-truncate disabled; tamper test | no in-place edit | ⬜ |
| LOG-04 | Logs free of secrets/PII (mac→hash, no tokens) | A | grep audit/journal for token/cookie/key patterns | none | 🔄 |
| LOG-05 | Audit survives service crash + reboot | A | crash mid-write → log consistent | intact | ⬜ |
## 7. Configuration management & rollback (4R / double-buffer)
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| CFG-01 | Sensitive config change = shadow→validate→atomic swap | A | `secubox-params swap` flow; partial write never live | atomic | ⬜ |
| CFG-02 | 4R rollback restores prior state (R1..R4 snapshots) | A | mutate → `rollback --target R1` → state == pre | restored | ⬜ |
| CFG-03 | Validation rejects bad config before swap (4R: Read→Write→Validate→Rollback/Commit) | A | inject invalid → swap refused, live unchanged | refused | ⬜ |
| CFG-04 | Config swap audit-logged + (ZKP-gated where required) | A | swap → audit line | logged | ⬜ |
## 8. Update mechanism
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| UPD-01 | APT repo GPG-signed; unsigned/altered pkg refused | A | tamper a .deb → `apt` refuses | refused | 🔄 |
| UPD-02 | Upgrade preserves runtime state + restarts services (no outage) | A | upgrade → portal up, kbin 200, nft intact (regression of #581) | no downtime | ✅ |
| UPD-03 | Downgrade / rollback path defined | D+A | pinned prior version installs cleanly | works | ⬜ |
| UPD-04 | Reproducible build / provenance | A | CI build hashes recorded per release | recorded | 🔄 |
## 9. Data protection at rest
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| DAT-01 | Secrets only under `/etc/secubox/secrets` 0600, svc-owned | A | inventory + `stat` | conforms | 🔄 |
| DAT-02 | No secrets in code / TOML / git history | A | `git log -p` + `git grep` secret patterns | none | 🔄 |
| DAT-03 | SQLite stores hashed identifiers (mac_hash, cookie_id_hash), not raw PII | A | schema + sample-row audit | hashed | 🔄 |
| DAT-04 | Data retention enforced (social 7d, logs rotation) | A | retention timers prune | enforced | 🔄 |
## 10. Resilience / fail-safe
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| RES-01 | Service crash → auto-recovery (watchdog), portal probe | A | kill portal → restored + kbin 200 | recovers | ⬜ |
| RES-02 | RAM-pressure: no OOM cascade under load (Armada budget) | M+A | load test; per-service MemoryMax; no thundering-herd | stable | 🔄 |
| RES-03 | Fail-secure: filter/addon error must not open the WAF or break pages | A | inject addon exception → default-drop / fail-open page-safe | secure | 🔄 |
## 11. Hardening / vulnerability management
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| HRD-01 | No known-vuln Python deps | A | `pip-audit` / safety in CI | 0 high/critical | ⬜ |
| HRD-02 | No known-vuln OS packages in the image | A | `debsecan`/trivy on the image | 0 high/critical | ⬜ |
| HRD-03 | Attack-surface minimal: unused services disabled | A | enabled-units ∩ required set | minimal | 🔄 |
| HRD-04 | SAST clean on the codebase | A | `bandit` (py) in CI | no high | ⬜ |
| HRD-05 | Pentest of the exposed surface (kbin, HAProxy, R3) | M | grey-box assessment report | no critical | ⬜ |
## 12. Conformity glue (CI)
| ID | Requirement | Type | Method / assertion | Pass | St |
|----|-------------|------|-------------------|------|----|
| CI-01 | `tests/cspn/` runs in CI, gates merge | A | workflow job red on fail | gating | ⬜ |
| CI-02 | Coverage ≥80% on security-critical modules | A | coverage report | ≥80% | ⬜ |
| CI-03 | `compliance-lint` (AppArmor/user/secrets/no-bypass/SPDX) per PR | A | linter job | clean | 🔄 (SPDX only) |
---
## How to operationalise
1. Write the **cible de sécurité** (ST-01) — everything else traces to it.
2. Scaffold `tests/cspn/` (pytest), one module per theme above
(`test_crypto.py`, `test_authz.py`, `test_firewall.py`, `test_audit.py`,
`test_rollback.py`, …). Each `XXX-NN` ID = one test function id.
3. Add a CI job (CI-01) running it against a built image / a staging board.
4. Add `compliance-lint` (CI-03) for the static rows (perms, AppArmor,
no-bypass, SPDX, no-secrets).
5. Burn down ⬜→✅; the ✅ rows above are already verifiable today.
Priority order (highest CSPN risk first): **§6 audit immutability**, **§7
4R rollback**, **§3 AppArmor enforce + privilege**, **§1 TLS**, **§12 CI
gate/coverage** — these are the criteria most likely to fail an assessment
today given the current ~9% test coverage.

View File

@ -1,101 +0,0 @@
# 🪶 Research-Note Poster — *Gondwana Trust Substrate*
### Sketch-design architectural poster · vulgarized · last-day evolutions + roadmap
*2026-07-01 · CyberMind / SecuBox-Deb*
This note ships two things:
1. **A ready-to-paste image-generation prompt** (for GPT-Image / Midjourney / SDXL) that renders the poster.
2. **An ASCII layout mock** so the composition is concrete, plus the vulgarized copy the poster carries.
---
## 1 · The image-generation prompt (paste this)
> **A hand-drawn architectural research-note poster, "sketchnote" / engineering-lab-notebook style** — like a brilliant hacker's whiteboard photographed at 2 a.m. Ink-and-marker on aged parchment, cosmos-black background (#0a0a0f) with a subtle hexagonal grid, hand-lettered headings in a Cinzel-esque serif, body notes in a monospace "JetBrains Mono" hand, arrows drawn freehand.
>
> **Palette:** gold-hermetic (#c9a84c) for titles + borders, matrix-green (#00ff41) for "it works / live", cyber-cyan (#00d4ff) for data flows, cinnabar-red (#e63946) for "guarded / danger", void-purple (#6e40c9) for future/roadmap. Faint alchemical/hermetic marginalia (small circuit-sigils, a mirror glyph 🪞, tiny onion for Tor).
>
> **Title band (top):** big gold hand-lettering — **"GONDWANA · A VILLAGE OF BOXES THAT TRUST BY MATH, NOT BY FAITH"**. Subtitle in small caps: *"self-certifying mesh · federated services · lend-a-service macros"*.
>
> **Three stacked strata drawn as geological layers of a mirror (the sketch's spine):**
> - **Layer 1 — THE HANDSHAKE (green):** two little box-characters shaking hands; above them a speech bubble *"my name = the hash of my key"*; a rejected forger box crossed out in red with *"can't fake a name you can't compute"*.
> - **Layer 2 — THE CATALOG (cyan):** a hand-drawn shop-shelf / market-stall labeled *"Service Registry"*; shelves hold little cards ("WAF mirror", "Suricata", "Tor exit"); an arrow *"Auto register all"* pulling neighbor stalls' cards onto your shelf.
> - **Layer 3 — LEND-A-SERVICE MACROS (gold+onion):** a box lending a glowing **onion (Tor exit)** across a rope-bridge (the mesh) to a neighbor box, who plugs it in and their traffic pops out somewhere far away. Little padlock-robot (AppArmor) guarding the rope.
>
> **A freehand proof strip across the middle (green, like a lab result taped on):** a comic 4-panel: (1) gk2 pins a "Tor exit for rent" flyer, (2) flyer flies to c3box over a dotted mesh line, (3) c3box taps it → a guard checks a signed ticket → opens a tiny gate, (4) c3box's web traffic exits as a masked Tor node — caption in green marker: **`{"IsTor": true}` — PROVEN LIVE**.
>
> **Right margin — "THE BOUNCER" side-sketch (red):** a stern padlock-bouncer with a checklist: *"√ real name (hash) · √ signed ticket · √ from the mesh only · √ one door, one guest · everything else: DENIED"*. Small note: *"the review robots caught ~10 booby-traps before the doors opened"*.
>
> **Bottom third — "HORIZON / ROADMAP" as a dotted mountain trail into a purple sunrise:** milestone flags along the trail — *"more lendable services: VPN-relay · DNS · mirror"*, *"zero-knowledge secret handshake (GK·HAM)"*, *"ask-permission mode (pending)"*, *"two-way bridges + native Tor everywhere"*. A tiny hiker box walking toward them.
>
> **Overall vibe:** warm, playful, hand-crafted, a little hermetic/alchemical, highly legible, poster-ratio (2:3 portrait), lots of arrows and margin doodles, feels like a research lab's celebratory wall poster. No photorealism — pure ink-sketch + marker.
---
## 2 · ASCII layout mock (the composition)
```
╔══════════════════════════════════════════════════════════════════╗
║ 🪞 G O N D W A N A — boxes that trust by MATH, not by FAITH ║ ← gold title
║ self-certifying mesh · federated services · lend-a-service ║
╠══════════════════════════════════════════════════════════════════╣
║ ▓ LAYER 1 · THE HANDSHAKE (green = works) ║
║ [box]🤝[box] "my name = hash(my key)" ║
║ [forger]✗ "can't fake a name you must compute" ║
║------------------------------------------------------------------║
║ ▓ LAYER 2 · THE CATALOG (cyan = data flows) ║
║ ┌shelf: Service Registry┐ ◀── "Auto register all" ║
║ │ [WAF][Suricata][Tor 🧅]│ pulls neighbours' cards to you ║
║------------------------------------------------------------------║
║ ▓ LAYER 3 · LEND-A-SERVICE MACROS (gold + 🧅) ║
║ gk2 [🧅]══rope-bridge (mesh)══▶ [box] c3box 🔒(AppArmor guard) ║
║==================================================================║
║ ✅ PROOF STRIP (taped lab result): ║
║ gk2 posts "Tor exit for rent" → flies to c3box → guard checks ║
║ signed ticket → opens gate → c3box exits as Tor {IsTor:true} ║
╠══════════════════════════════════╦═══════════════════════════════╣
║ ⛰ HORIZON / ROADMAP (purple) ║ 🔒 THE BOUNCER (red) ║
║ ·→ more services: VPN·DNS·mirror ║ √ real name (hash) ║
║ ·→ zero-knowledge handshake HAM ║ √ signed ticket ║
║ ·→ ask-permission (pending) mode ║ √ from the mesh only ║
║ ·→ two-way bridges + Tor native ║ √ one door / one guest ║
║ 🥾 …hiker box walking the trail ║ ✗ everything else: DENIED ║
╚══════════════════════════════════╩═══════════════════════════════╝
```
---
## 3 · The vulgarized story the poster tells
**The one-line pitch.** *A cluster of little security boxes (SecuBoxes) that don't need a boss or a
central authority to trust each other — their name literally IS a fingerprint of their key, so nobody
can impersonate anyone. On top of that trust, boxes publish a menu of services and can even lend each
other real capabilities — like one box lending its Tor exit so another box's traffic comes out
anonymized, far away.*
**What changed in the last day (three layers, all live on gk2 + c3box):**
| Layer | Plain-language | Under the hood |
|------|----------------|----------------|
| 🤝 **Handshake** | "My name is the math of my key — you can check it, you can't fake it." | did:plc self-cert; `ingest_offer` checks `did == hash(pubkey)` before trusting anything |
| 🛒 **Catalog** | "See every box's menu on one shelf; one click subscribes you." | p2p Service Registry = live view of the federated annuaire catalog + "Auto register all" |
| 🧅 **Lend-a-service** | "Rent my Tor exit / my relay — the guard only opens for a signed ticket." | `secubox-macro` (macroctl + tor-exit plugin, AppArmor-confined, nft-gated grant) |
**The headline proof.** gk2 offered its Tor exit as a service → it federated to c3box → c3box subscribed,
activated, and pulled a *signed* grant over the mesh → gk2's firewall opened just for c3box's mesh IP →
**c3box's traffic now exits through gk2's Tor node** (`{"IsTor":true}`). End-to-end, across two real
machines, with a hardened privilege chain (unprivileged app → tight sudo → root dispatcher → confined
plugin → firewall). The adversarial review loop caught **~10 critical booby-traps** before any of it shipped.
**The bouncer (why it's safe).** Every request is checked at the right door: a real (hash-derived) name,
a signature that only the key-owner could make, coming *only* from the mesh, opening exactly one port for
exactly one guest — and anything unexpected is denied by default (CSPN posture).
**Where the trail leads (roadmap).**
- 🧩 **More lendable services** — the same framework, new plugins: `wg-relay` (VPN), `dns-resolver`, `http-mirror`.
- 🕵️ **Zero-knowledge handshake** — swap the current stubs for the real GK·HAM Hamiltonian NIZK, so boxes prove things about themselves *without revealing secrets*.
- 🙋 **Ask-permission mode** — today lending is auto-approved; next, a provider can hold a request as *pending* and approve it, which needs cross-box approval federation.
- 🌉 **Two-way bridges + Tor everywhere** — finish the reverse mesh path and put a native Tor on every box so any box can be a full exit provider out of the box.
---
*Notebook margin, small hand: "the mirror shows the mesh; the mesh shows the mirror. — 🪞"*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Some files were not shown because too many files have changed in this diff Show More