Compare commits

...

228 Commits

Author SHA1 Message Date
7206350c34 docs(gondwana): fix P1 plan version bump 1.7.2->1.7.6 (1.7.5 already shipped)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-29 10:38:39 +02:00
29ac8c311c docs(gondwana): Phase 1 implementation plan (mesh substrate)
10-task TDD plan: api/mesh.py pure logic (collision guard, p2p.toml loader,
master-assigned IP, wg.conf parse/render/adopt, DDNS name), wire into
api/main.py (defaults 10.100.0.0/24->10.10.0.0/24, 51820->51822), root
sbx-mesh-up provisioner, packaging (1.7.2, wireguard-tools dep), then
zero-disruption cutover on gk2 (key adoption preserves c3box handshake)
and amd64 enrollment as .3 with verification.
2026-06-29 10:38:10 +02:00
74959276b6 docs(gondwana): rendezvous-as-role + per-node DDNS + distributed directory
Fold two design refinements into the roadmap without expanding Phase 1:
- Rendezvous is a role any node can hold (gk2 active now, not hardwired);
  each node carries a DDNS name <boxname>.secubox.in as part of identity;
  registry is local-first/replicable. Phase 1 = forward-compat only;
  availability-based floating failover is Phase 4.
- Cross-cutting distributed directory: shared state (peers/services/threats/
  names) migrates to a replicated DNS-structured append-only ledger
  (did:plc / Chain of Hamiltonians) underpinning Phases 2-4.
- Phase 4: per-node auto-registration -> <service>.<boxname>.secubox.in
  vhosts, DNS to gk2 public IP, Host-routed over wg-mesh.
2026-06-29 10:33:19 +02:00
d61d585f91 docs(gondwana): master_endpoint configurable (DDNS-ready), pinned to public IP
Satellite-facing endpoint is free-form host:port — accepts a DDNS hostname
(WireGuard re-resolves per handshake, survives WAN IP changes) or a literal
IP. Current deployment pins 82.67.100.75:51822; DDNS is a later one-line
config swap.
2026-06-29 10:29:13 +02:00
3fa951017b docs(gondwana): Phase 1 mesh substrate design spec
Multi-site WireGuard substrate + node identity for the gondwana mesh:
adopt secubox-p2p as the single mesh owner, fix the 10.100.0.0/24 ->
10.10.0.0/24 subnet collision with br-lxc, gk2 = public rendezvous (UDP
51822), hub-and-spoke routing, master-assigned addressing, persistent
per-node identity for the Phase 2 ZKP layer. Zero-disruption cutover via
private-key adoption. Phases 2-4 (ZKP enrollment, signed protection
sharing, service mirroring) scoped out for their own cycles.
2026-06-29 10:27:36 +02:00
9c7cd79e58 fix(lyrion): slimproto DNAT bound to dead lan0 — make interface-agnostic
ensure_slimproto_dnat() hardcoded iifname "lan0", but on a SecuBox
behind another router (gk2 behind a Freebox) lan0 is DOWN and LAN players
arrive on the uplink (eth2) — so the prerouting DNAT never matched and 0
players could reach the Lyrion LXC on TCP/UDP 3483.

Generate an interface-agnostic rule (iifname != br-lxc) that matches
players on any LAN/Wi-Fi/uplink interface; SECUBOX_LAN_IFACE still pins a
single interface. Forward policy is accept and conntrack rewrites the
reply (LXC gateway = host), so no forward rule is needed.

Verified live on gk2: LAN clients now reach gk2:3483 -> 10.100.0.100:3483.
Bumps secubox-lyrion 1.1.0 -> 1.1.1.
2026-06-29 10:19:44 +02:00
658ae8a368 fix(hub): resolve netstats import crash-loop
hub/api/main.py imported netstats as a top-level module, but netstats.py
ships inside the api/ package and the service runs uvicorn api.main:app
with WorkingDirectory=/usr/lib/secubox/hub — so the import failed with
ModuleNotFoundError and secubox-hub crash-looped (~9000 restarts on the
live USB image). Use a relative import with a top-level fallback (the
collector adds api/ to sys.path explicitly, so it keeps working).

Bumps secubox-hub 1.5.0 -> 1.5.1.
2026-06-29 06:23:16 +02:00
fb07e679e8 Merge branch 'fix/remove-authelia-sso' — remove Authelia SSO 2026-06-29 06:14:31 +02:00
f0a284e36b feat(authelia): remove SSO entirely — permissive no-op gate
Authelia SSO is removed. nginx/authelia.conf is reduced to a permissive
no-op: /__sbx_auth_verify returns 200 for every request and
@sbx_auth_login falls back to the app root. The two named locations are
kept only because grafana/lyrion/yacy/rustdesk/fmrelay/zigbee/nextcloud
vhosts still reference them; without them nginx fails to load.

postinst now disables + masks secubox-authelia.service instead of
enabling it. No SSO portal, no session check, no Authelia socket.

Fixes the dead sso.gk2.secubox.in 302 that produced a password prompt
on LAN clients (live USB at 192.168.1.9). Apps keep their native auth;
exposure boundaries stay with HAProxy + WAF.
2026-06-29 06:14:27 +02:00
c3cfd512d7 fix(image): keep http-level nginx conf.d (geo/map), only strip location-only
The live-usb nginx cleanup deleted ALL conf.d/*secubox*.conf, including
secubox-lan-geo.conf which defines 'geo $lan_client'. authelia.conf then hit
'unknown lan_client variable' -> nginx -t fails -> no web server -> blank
kiosk with a connection error. Only remove conf.d files that actually contain
a location block (misplaced); keep geo/map/limit_req_zone/upstream.
2026-06-29 06:05:00 +02:00
99af60bc16 fix(image): don't abort USB build on invalid nginx config (set -e trap)
Some checks are pending
License Headers / check (push) Waiting to run
The 'final nginx cleanup' branch runs precisely when nginx -t fails, but
nginx_error=$(nginx -t|head) and missing_file=$(...grep...) then trip
set -e/pipefail and abort the whole build right after the warn. Guard both
with || true. nginx config is regenerated at first boot by secubox-net-detect,
so a build-time-invalid config is non-fatal. This was the last blocker on the
amd64 USB (kiosk + everything else already pass).
2026-06-28 11:27:16 +02:00
854805fbbd fix(image): build-live-usb.sh accept --kiosk (was only --no-kiosk)
The workflow passes --kiosk (consistent with build-rpi-usb.sh); build-live-usb.sh
only had --no-kiosk and erred 'Unknown argument: --kiosk', failing the x64 USB
build. Add a --kiosk case (INCLUDE_KIOSK=1).
2026-06-28 11:14:19 +02:00
b945c831a0 fix(image): policy-rc.d so the kiosk (X11/chromium) installs in chroot
The live-usb kiosk stack (dbus, X11, chromium) aborted its postinst in the
init-less chroot ('Failed to connect to system message bus', invoke-rc.d
errors), failing the build. Add /usr/sbin/policy-rc.d (exit 101) before the
installs and remove it before squashfs, so packages don't try to start
services at build time but the booted system still does. Keep kiosk ON for
amd64 USB (extra_args=--kiosk). Do NOT disable kiosk.
2026-06-28 11:05:08 +02:00
2b52eaa330 fix(image): global dpkg force-confold in live-usb chroot (mesh.toml prompt)
The per-install flag didn't cover secubox-mesh's configure path; write
/etc/dpkg/dpkg.cfg.d/90-secubox-confold (force-confold/confdef) into the chroot
before any install so every dpkg op keeps conffiles and never prompts. Fixes
'end of file on stdin at conffile prompt' aborting the amd64 USB build.
2026-06-28 10:45:16 +02:00
f3fc9a3a92 fix(image): non-interactive conffile handling for secubox-full install
Some checks are pending
License Headers / check (push) Waiting to run
secubox-mesh's mesh.toml is an auto-detected conffile that triggers an
interactive dpkg prompt (*** mesh.toml [Y/I/N/O/D/Z]) during configure,
failing the headless chroot install in build-live-usb.sh (amd64 USB build).
Install secubox-full with DEBIAN_FRONTEND=noninteractive + --force-confold/
--force-confdef + a follow-up dpkg --configure -a, in build-live-usb.sh and
build-image.sh.
2026-06-28 09:15:35 +02:00
4398ebe654 Merge #760: LAN 192.168.10.0/24 standardization + build-pipeline fixes (dearmor, golang-go, arch=both, -d) + 1.10.0 2026-06-28 09:10:47 +02:00
1a60f82de9 fix(ci): build pure-Go pkgs with -d (golang-go metapackage dep-check) (ref #760)
dpi/toolbox-ng/waf-ng are CGO_ENABLED=0 cross-compiles; the go toolchain is
present (golang-1.22-go) but dpkg-checkbuilddeps trips on the golang-go
metapackage. Skip the dep check for just these three (-mod=vendor, self-contained).
2026-06-28 09:10:45 +02:00
bb9208cbcc Merge p2p fixes: master-link statedir, www path, no-self-peer, invite ownership (#762 substrate) 2026-06-28 09:09:08 +02:00
119771eadb fix(p2p): sbx-mesh-invite re-owns token store to secubox when run as root
Root-owned tokens.json made the secubox-user p2p (and the in-process
aggregator) crash with EACCES. chown -R secubox the master-link dir if root.
2026-06-28 09:06:36 +02:00
973c9e3a78 fix(p2p): don't count/list the local node as a peer
list_peers inserted+persisted get_self_peer() into PEERS_FILE, so peer_count
and online_peers counted the node itself (showed '<host> (local)' as a phantom
peer). /status and /peers now exclude is_local/self; self info stays available
via /discover/self for announcements.
2026-06-28 08:47:41 +02:00
675c4806ea fix(p2p): install web UI to /usr/share/secubox/www (fixes Module Not Found)
p2p shipped its UI to /var/www/secubox/p2p, but the dashboard vhost serves
modules from /usr/share/secubox/www (root + try_files). So /p2p/ fell through
to the unknown-module page. Install p2p + master-link UIs there like all other
modules; update nginx aliases to match.
2026-06-28 08:27:07 +02:00
255677c219 fix(p2p): postinst creates /var/lib/secubox/p2p/master-link owned secubox
The app's master-link init mkdir's master-link/ and failed EACCES when
/var/lib/secubox/p2p was root-owned (it was never created by the package),
500-ing /api/v1/p2p/status and breaking mesh-join enrollment. Create p2p/ +
master-link/ as secubox in postinst (mirroring the log-dir pattern) and chown
to repair pre-existing installs. Never touches the shared parent (#494/#511).
2026-06-28 08:22:54 +02:00
8d5627567d fix(ci): build-packages arch=both must expand to both arches (ref #760)
The discover matrix filter only compared requested_arch against amd64/arm64/
empty, so the 'both' workflow_dispatch option produced an EMPTY matrix (no
builds, collect failed). Normalize 'both' -> empty (= all arches).
2026-06-28 06:57:25 +02:00
8bbc573149 fix(ci): install golang-go so arm64 pure-Go packages build (ref #760)
secubox-dpi and secubox-toolbox-ng are CGO_ENABLED=0, GOARCH=arm64,
-mod=vendor offline cross-compiles. They failed the arm64 build only
because golang-go was never installed, so dpkg-checkbuilddeps aborted on
their `Build-Depends: golang-go (>= 1.22)`. Not a CGO cross-toolchain
gap. ubuntu-24.04 ships golang-go >= 1.22; installing it lets the pure-Go
arm64 cross-compile run on the amd64 runner.

Unblocks a complete amd64+arm64 package set for the apt repo + arm64 images.
2026-06-28 06:52:40 +02:00
6a557cbe2c fix(image): dearmor apt keyring for signed-by (apt rejects armored) (ref #760)
build-image.sh / build-installer-iso.sh / validate-staged-repo.sh fetched the
ASCII-armored secubox-keyring.gpg and used it directly as
`signed-by=/usr/share/keyrings/secubox.gpg`. apt's signed-by requires a
DEARMORED (binary) keyring, so the chroot apt-get update failed with
"NO_PUBKEY 44E50F0178E8BC7E / repository not signed" even though the repo
signature and key are correct. Pipe the key through `gpg --dearmor`.

Root cause of all image-build failures since the keyring became armored;
not a key desync (repo signer == published keyring == 44E50F0178E8BC7E).
2026-06-27 17:44:13 +02:00
a52af3b50a feat(netplan): standardize SecuBox LAN to 192.168.10.0/24; bump 1.10.0 (ref #760)
Default br-lan 192.168.1.1/24 collided with common ISP-router LANs
(Freebox/Livebox 192.168.1.0/24) when the appliance sits behind one:
WAN(DHCP)+LAN land on the same subnet -> duplicate route, ARP ambiguity,
unreachable mgmt IP. Observed live on c3box behind a Freebox.

- All board netplans (mochabin, espressobin-v7/ultra, x64-vm, x64-live)
  + VM LANs (vm-x64, vm-arm64) -> br-lan/LAN 192.168.10.1/24
- Generators: secubox-netmodes (inline + router.yaml.j2 template),
  secubox-hub preview, secubox-net-detect
- dnsmasq (espressobin-v7.conf): dhcp-range + option:router + dns-server
- live-usb build scripts + self-signed cert SANs -> IP:192.168.10.1
- Out of scope (untouched): 192.168.255.1 mgmt/trusted-proxy whitelist,
  WAN-probe GATEWAYS lists, remote-ui/round + tests
- Minor "medium" bump 1.9.0 -> 1.10.0 (core build scripts; mochabin-live
  stays on its 2.0.0 track)

Live: c3box br-lan already 192.168.10.1/24 + netmodes template aligned;
gk2 WAN moved to eth2 DHCP @ .200 (Freebox reservation on eth2 MAC).
2026-06-27 17:11:23 +02:00
47076b24d3 fix(netplan): mochabin — copper eth2 as 2nd DHCP WAN candidate, out of br-lan
Some checks are pending
License Headers / check (push) Waiting to run
The image labelled eth0 (SFP+) as the only WAN; the single copper RJ45
(eth2/mvpp2-2) was trapped in br-lan, so an operator/uplink arriving on
copper got no lease. Make both eth0 (SFP+) and eth2 (copper) DHCP WAN
candidates (whichever is cabled gets the lease); br-lan keeps the switch
LAN ports. Verified live on the c3box reference install.
2026-06-27 16:27:05 +02:00
cb6eee4b0b docs: netboot validation + c3box first install to Debian + #748 (ref #748 #737)
- HISTORY: 2026-06-27 milestone — gk2→c3box netboot proven, first SecuBox Debian
  install on a physical MOCHAbin, #748 wget scissor documented, boot.scr workaround
- WIP: update to 2026-06-27; c3box=reference install DONE; #748 deferred; rig teardown pending
- TODO: T5 section — proper bootloader (extlinux addr fix + #748 paths), package the
  netboot signed-install flow, rig teardown checklist
- wiki/Netboot-Install.md: new reference page — prereqs, TFTP steps, signed install,
  boot.scr persistence fix, gotchas table, eMMC layout
- wiki/_Sidebar.md: link Netboot-Install under BOOT section
2026-06-27 16:27:05 +02:00
CyberMind
5d505cae16
Merge PR #759 — nft-based network stats dashboard (#758)
nft-based network stats — log/counter drops, attacks, ad-blocks, in/out traffic into the dashboard
2026-06-27 16:25:52 +02:00
1c6a978748 feat(toolbox): add Réseau dashboard tab with nft throughput sparklines (ref #758) 2026-06-27 11:35:52 +02:00
9696cdad13 feat(toolbox): network_drops sourced from hub netstats snapshot (ref #758) 2026-06-27 11:31:16 +02:00
fd9f535e63 fix(hub): require JWT on netstats endpoints (ref #758)
Add user=Depends(require_jwt) to netstats_summary and netstats_series,
matching the sibling pattern used throughout the same router.
Drop redundant int() casts on already-typed window/step params.
2026-06-27 11:29:00 +02:00
d91f6a8b67 feat(hub): /netstats/summary + /netstats/series endpoints (ref #758) 2026-06-27 11:26:06 +02:00
c1c4810f3e feat(hub): package netstats collector, timer, nft tap deploy, sudoers (ref #758) 2026-06-27 11:22:04 +02:00
d05deee7ff feat(hub): netstats snapshot builder + privileged collector + root wrapper (ref #758) 2026-06-27 11:16:38 +02:00
b30a69316a fix(hub): robust max_ts guard + multi-name category aggregation test (ref #758)
- Replace truthiness guard (max_ts_c or max_ts_i) with explicit None-check
  any(t is not None ...) so a sole sample at ts=0 (boot edge) is handled by
  intent, not coincidence.
- Add test_query_drops_sums_multiple_names_per_category: proves blacklist_v4
  and blacklist_v6 (same category) are summed (10+5=15) in the same bucket,
  not overwritten.
2026-06-27 11:13:58 +02:00
851af7d172 feat(hub): netstats SQLite store + reset-aware series query (ref #758) 2026-06-27 11:10:43 +02:00
55814ac3a0 feat(hub): netstats pure parsers (proc/net/dev, nft counters, reset-aware delta) (ref #758) 2026-06-27 11:07:27 +02:00
e6260215a4 feat(hub): inet filter input policy-drop nft tap counter (ref #758) 2026-06-27 11:04:27 +02:00
6ae107db91 feat(mitmproxy): named nft counter on WAF rate-limit drops (ref #758) 2026-06-27 11:02:06 +02:00
60f876ad08 feat(toolbox): named nft counters in blacklist spine + named-counter reader (ref #758) 2026-06-27 10:58:58 +02:00
bdafced25a docs(netstats): implementation plan — 11 TDD tasks (ref #758) 2026-06-27 10:54:47 +02:00
5b283b6bff docs(netstats): design spec — nft-based network stats dashboard (ref #758) 2026-06-27 10:14:23 +02:00
740cbd291f release: toolbox-ng 0.1.26 (#757 reval nudge) + toolbox 2.7.24 (Liveness card removed)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-27 09:46:14 +02:00
d203b9aa8f Merge branch 'feature/757-sw-revalidation-nudge' — SW revalidation nudge (#757) + remove Liveness card
#757: for sw-neuter allow-listed hosts, strip If-None-Match/If-Modified-Since on
HTML document fetches so a stale-while-revalidate SW re-fetches a full 200 (banner
injected) and caches a banner'd shell without being neutered. Reviewed APPROVED.
Also removes the redundant ♥ Liveness dashboard card (version stays in the top badge).
2026-06-27 09:45:19 +02:00
52d358c9d8 ui(toolbox): remove redundant Liveness card — version stays in the top badge (ref #757) 2026-06-27 09:44:57 +02:00
20fca011a1 feat(sbxmitm): SW revalidation nudge — strip If-None-Match/If-Modified-Since for allow-listed HTML fetches (ref #757)
Add requestWantsHTML (Sec-Fetch-Dest: document or Accept: text/html) and use
it in mitmPipeline to strip conditional headers before up.Do for sw-neuter
allow-listed hosts, forcing a full 200 so the injected banner is cached by a
stale-while-revalidate Service Worker.
2026-06-27 09:42:13 +02:00
fb90349670 docs(sbxmitm): plan — SW revalidation nudge (ref #757) 2026-06-27 09:40:32 +02:00
90a7df6f4b release: toolbox-ng 0.1.25 + toolbox 2.7.23 — #755 #ads breakdown + #756 cosmetic scroll 2026-06-27 09:19:37 +02:00
f997d1c9a9 Merge branch 'feature/755-ads-aggregate' — #ads MITM-stats breakdown (#755) + cosmetic scroll-restore (#756)
#755: the #ads card becomes an honest labeled breakdown — Pubs bloquées (204),
Trackers détectés (social_edges distinct cookie-trackers), Pages nettoyées (new
sbxmitm cosmetic counter, wg-gated), Drops réseau (blacklist nft). #756: the
cosmetic ad-hide style restores html,body overflow so paywall scroll-locks
(Bloomberg) no longer leave the page unscrollable. Final review: one wg-gate fix
applied; race-clean, contract airtight end-to-end.
2026-06-27 09:18:27 +02:00
24fa9da107 fix(sbxmitm): gate recordCosmetic() on wg to stop over-counting pages_cleaned (ref #755)
Non-WG LAN clients never receive the cosmetic ad-hide <style> (injectHTML
gates injectCosmetic behind `if wg`), yet recordCosmetic() fired on every
successful inject — inflating pages_cleaned for banner-only flows.
Gate the call with `if wg && px.ads != nil` to match injectHTML's own gate.
2026-06-27 09:17:49 +02:00
ff439d9395 feat(toolbox): cosmetic-pages counter → #ads 'Pages nettoyées' (ref #755) 2026-06-27 09:10:37 +02:00
6100a3e8ed feat(toolbox): #ads breakdown — trackers_seen + network_drops (ref #755)
Co-Authored-By: Gérald Kerma <devel@cybermind.fr>
2026-06-27 09:05:22 +02:00
3473320ad0 fix(sbxmitm): cosmetic restores scroll (overflow:auto) — paywall scroll-lock left Bloomberg dead (ref #756) 2026-06-27 09:02:43 +02:00
3e8b3e80fd docs(toolbox): implementation plan — #ads aggregate MITM stats (ref #755) 2026-06-27 08:54:57 +02:00
a76da4f783 release: toolbox-ng 0.1.24 + toolbox 2.7.22 — SW-neuter #753 2026-06-27 08:43:21 +02:00
ad4fc51d21 Merge branch 'feature/753-sw-neuter' — targeted Service-Worker neuter for the R3 banner (ref #753)
PWA news sites (leparisien, cnn, 20minutes, franceinfo) serve their main HTML
from a Service-Worker cache so the navigation never reaches the MITM and the
banner can't be injected. For an operator-curated allow-list, sbxmitm answers
the SW script fetch with a passive self-unregistering SW (next navigation reaches
the MITM → banner), with an auto-learn candidate feed to /__toolbox/sw-candidate.
Targeted-strict: empty list = no-op. Final review READY TO MERGE (race-clean).
2026-06-27 08:41:52 +02:00
bcea1ea4ac chore(toolbox): drop unused json import in sw-candidate test (ref #753) 2026-06-27 08:41:52 +02:00
f6d2e44565 feat(toolbox): SW-neuter auto-learn flush + /__toolbox/sw-candidate ingest (ref #753) 2026-06-27 08:33:17 +02:00
634a08c3ab feat(sbxmitm): wire SWNeuter into mitmPipeline + --sw-neuter-hosts (ref #753) 2026-06-27 08:27:59 +02:00
690da98510 fix(sbxmitm): SW-neuter reload throttle + test copyright header (ref #753)
- swneuter.go: use reload.DefaultReloadThrottle (15s) instead of 0 in
  newSWNeuter — avoids stat on every Maybe() call, consistent with policy.go
- swneuter_test.go: add missing copyright line to match implementation header
2026-06-27 08:25:37 +02:00
f94841e34f feat(sbxmitm): SWNeuter — allow-list + self-unregistering SW body (ref #753) 2026-06-27 08:22:43 +02:00
fc8248b854 docs(toolbox-ng): implementation plan — targeted SW-neuter (ref #753) 2026-06-27 08:19:59 +02:00
b3c1db9380 docs(toolbox-ng): design spec — targeted SW-neuter for the R3 banner (ref #753) 2026-06-27 08:13:13 +02:00
72e8cbd2db release(toolbox-ng): 0.1.23 — rebuild master (#751 nonce-CSP) + SBX_DEBUG_CSP 2026-06-27 08:02:45 +02:00
827165e6fd release(toolbox): 2.7.21 — bundle.py banner reconciliation (#754) 2026-06-27 07:57:09 +02:00
0d906b1471 Merge branch 'fix/754-bundle-reconcile' — bundle.py to #740 DOM-API banner + R4 + #752 guard (ref #754)
Brings master's bundle.py up to the working board version: the #740 mk() DOM-API
banner (Trusted-Types-proof — why x.com/news render), the R4 analyst tier (#736)
folded into its level switch, and the #752 top-frame guard (no banner in 3rd-party
iframes). Folds in fix/752. Reviewed APPROVED (6/6 named risks clean, 179 tests).
2026-06-27 07:54:25 +02:00
d1607328fd fix(toolbox): reconcile bundle.py to master — #740 DOM-API banner + R4 tier + #752 top-frame guard (ref #754)
The board ran the #740 DOM-API (mk(), Trusted-Types-proof) banner from the
unmerged feature/740 branch; master had diverged with the R4 tier (#736) on an
innerHTML banner. This brings master's bundle.py up to the working board version
(mk() rendering — TT/strict-CSP-proof, why x.com/news render), adds the R4 tier
into the #740 level switch, and folds in the #752 top-frame guard (no banner in
3rd-party iframes). Updated 2 stale inline tests: the #653 'no fetch at load'
assertion is now '/__toolbox/bundle' not fetched — #740's toggle handlers fetch
/set-* on user click, which is not the SW-hijackable load-time bundle fetch.
2026-06-27 07:52:01 +02:00
aae47c6e2e Merge branch 'fix/751-sbxmitm-csp-debug' — SBX_DEBUG_CSP banner/CSP diagnostic (ref #751)
Root cause of the x.com/news banner failures was a STALE board sbxmitm binary
lagging master's #728 nonce-borrow; redeploying a master build fixed them.
This adds a permanent opt-in CSP diagnostic (SBX_DEBUG_CSP) to pinpoint why a
banner does/doesn't render on a given site.
2026-06-27 07:21:29 +02:00
4329ab2d7b feat(sbxmitm): SBX_DEBUG_CSP diagnostic log for banner/CSP visibility (ref #751) 2026-06-27 07:15:30 +02:00
c46e24f820 Merge branch 'fix/750-health-banner-spa-reinject' — health-banner SPA re-inject guard (ref #750)
Some checks are pending
License Headers / check (push) Waiting to run
First-party WAF-injected health banner: re-attach trigger+banner (and re-inject
styles) when an SPA rebuilds <body>, with a documentElement childList observer +
1.5s interval fallback. Parity with the R3 banner's existing self-heal.
NB: distinct from the x.com R3 kbin-banner nonce-CSP issue (#751).
2026-06-26 19:35:51 +02:00
3e9f6e8461 fix(hub): re-sync health-banner-open class on re-attach + bump 1.4.7 (ref #750) 2026-06-26 19:11:46 +02:00
4315584f79 fix(hub): health-banner SPA re-inject guard — re-attach on body wipe (ref #750) 2026-06-26 19:08:39 +02:00
1a8ed97cfe Merge branch 'feature/749-cookies-cross-site-tracker-detection' — cookies cross-site tracker panel (ref #749)
- toolbox: cookie_xsite_detail aggregation over social_edges (cross-site cookie-id reuse across >=2 first-party sites)
- toolbox: GET /admin/cookie-crosssite endpoint
- cookies dashboard: Trackers cross-site panel consuming the R3 social-graph
2026-06-26 18:50:45 +02:00
5cc97b1aea fix(cookies): coerce pre_consent_hits to int + await loadCrossSite in refresh (ref #749) 2026-06-26 18:27:16 +02:00
1f5c6ed3e3 feat(cookies): cross-site trackers panel from toolbox R3 (ref #749) 2026-06-26 18:24:19 +02:00
2a9350b9df feat(toolbox): GET /admin/cookie-crosssite endpoint (ref #749) 2026-06-26 18:20:28 +02:00
6f65a1936a feat(toolbox): cookie_xsite_detail aggregation over social_edges (ref #749)
Add _xsite_detail_from_conn() and cookie_xsite_detail() to social.py,
detecting (tracker_domain, cookie_id_hash) pairs reused across >=2 distinct
first-party sites. Mirrors aggregate() envelope. 7 tests green.
2026-06-26 18:13:39 +02:00
5c12063ca7 docs(cookies): implementation plan — cross-site tracker detection (ref #749) 2026-06-26 18:04:01 +02:00
11a0bbef66 docs(cookies): design spec — cross-site tracker detection surface (ref #749) 2026-06-26 17:58:36 +02:00
c8fe9bb148 fix(toolbox): clarify #ads labels — Trackers & pubs, bytes marked as estimate (ref #735)
The #ads panel mixes ad + tracker + telemetry blocks, and 'bytes saved' is a flat
~45 KB/block estimate (a blocked request is never downloaded, so real bytes cannot
be measured). Relabel 'Pubs bloquées' → 'Trackers & pubs bloqués' and mark the
byte figure as an estimate (~ + tooltip). Pairs with an operator allowlist update
excluding generic AWS API-gateway hosts (execute-api.*) from the ad classifier.
2026-06-26 17:42:31 +02:00
e87d46f6a7 feat(sbxwaf): inject the real SecuBox health banner (not a custom badge) into first-party HTML (#747)
Per operator intent, the WAF injects the SHARED secubox health-banner.js in its
CDN-injected mode (absolute Hub origin for the asset + metrics APIs via the
window.SECUBOX_* overrides) so the SAME health widget the dashboard shows mounts
on first-party content sites (chess.maegia.tv et al.) — NOT a bespoke badge nor
the toolbox/mitm kbin transparency banner. Skips pages that already ship the
banner; --widget-hosts now includes maegia.tv; --health-banner-origin configures
the Hub. CORS on the metrics API (access-control-allow-origin: *) is already set.
2026-06-26 17:33:53 +02:00
efac8cec16 feat(sbxwaf): inject SecuBox health/visit widget into first-party HTML (#747)
On operator-configured first-party host suffixes (--widget-hosts), the WAF injects
a discreet fixed-corner badge into text/html responses showing the live visit
counter + a protected mark. Decompression-aware (gzip/br/zstd), idempotent, strictly
fail-open (missing </body>, oversize, decode error → original bytes untouched).
Wired into both reverse-proxy ModifyResponse paths (cached + fallback).
2026-06-26 17:26:06 +02:00
9561cb4bdb fix(toolbox): Live metrics read cumulative stats (events table is empty under R3) (ref #744)
The toolbox.db  table was fed by the OLD Python mitmproxy addons; the R3
path is Go sbxmitm → relay → sidecars → cumulative, so that table is empty and the
Live-metrics panel showed all zeros. Fall back to the cumulative per-source totals
(cookies/ja4) when the events table is empty, and derive mitm.connections from the
ja4 handshake count (the cumulative has no 'dpi' key, so the old probe was always 0).
2026-06-26 17:18:46 +02:00
344bb0738d fix(crowdsec): nftables health detects custom secubox_blacklist table (firewall reported OK)
The firewall-bouncer uses a CUSTOM nft table (inet secubox_blacklist), not the
upstream default ip crowdsec / ip6 crowdsec6, so the legacy probe always reported
nftables not OK — propagating a false 'nftables firewall: not OK' to the
security-posture scorecard while the firewall (inet filter, default-drop) was
active. Detect the custom + default names; base nftables_ok on the general
SecuBox firewall being loaded, not the IPv6 anchor.
2026-06-26 17:14:56 +02:00
b54b5383cd fix(waf): show fresh engine data — gate CrowdSec overlay + dashboard tabs (ref #744)
The CrowdSec overlay existed because the OLD Python mitmproxy WAF log was usually
empty; the Go sbxwaf engine now writes a rich threat log, so the overlay was
clobbering the engine's fresh categories and pushing a stale '1h ago' entry onto
the live attack banner. Only overlay when the engine produced nothing. Also move
Tracked Attackers + Visits into dashboard tabs (Menaces / Attaquants / Visites).
2026-06-26 17:09:07 +02:00
23788e304b feat(waf-webui): Visits panel — client type / OS / geo / vhost bars (#747) 2026-06-26 16:59:49 +02:00
3b28f84591 feat(sbxwaf+waf-api): non-attacker visit statistics (client type/OS/geo) (ref #744 #747)
sbxwaf aggregates LEGITIMATE (non-blocked) traffic in memory — total, client-type
(browser/mobile-app/bot/crawler via UA), OS, per-vhost, status bucket, top client
IPs — and flushes a JSON snapshot every 30s (double-caching: hot path only bumps
counters). New --visits-stats flag. The WAF API /visits endpoint reads the snapshot
and geo-maps the top IPs (it holds the GeoIP DB) for the dashboard, no per-request
PII stored. A statusRecorder in the handler tallies every served response and
excludes the WAF-block 403 and unmapped 421.
2026-06-26 16:56:21 +02:00
e5f0d22dc6 fix(waf-api): crowdsec overlay must not crash the warm refresh (ref #744)
_overlay_crowdsec_stats received the already-decorated stats (top_countries as a
list of {country,count} dicts, not a dict) and did sorted(key=lambda x:-x[1]) →
TypeError on a str → aborted _refresh_warm_caches, freezing the warm cache and
zeroing the WAF dashboard's per-IP/vhost/tracked-attacker panels. Accept the
list shape, coerce counts to int, and wrap the overlay call defensively so a
CrowdSec hiccup can never wipe the WAF stats.
2026-06-26 16:18:58 +02:00
c6d6eb5c75 Merge #744: sbxwaf Go WAF engine + shared internal/ core
# Conflicts:
#	packages/secubox-toolbox-ng/cmd/sbxmitm/compress_test.go
#	packages/secubox-toolbox-ng/cmd/sbxmitm/cosmetic_test.go
#	packages/secubox-toolbox-ng/cmd/sbxmitm/gzip.go
#	packages/secubox-toolbox-ng/cmd/sbxmitm/gzip_test.go
2026-06-26 16:02:35 +02:00
2e6cec9b38 fix(waf-api): read sbxwaf threat-log path for the WAF dashboard (ref #744)
The Go sbxwaf engine writes the threat log to the sandboxed leaf dir
/var/log/secubox/waf/waf-threats.log; the dashboard's /stats + /alerts now
resolve that path (env-overridable, legacy-path fallback) so the WAF WebUI
shows real engine data again after the cutover.
2026-06-26 15:55:15 +02:00
b607d7f7d6 docs(waf-ng): package README (WIP) + ignore build artifacts (ref #744) 2026-06-26 15:31:26 +02:00
e5a2c5d287 fix(sbxwaf): final-review wave — vhost cache key, crowdsec LAPI url, body-inspect cap+audit, seccomp, trusted-host skip (ref #744)
Fix 1 (media-cache vhost isolation): key MediaCache.Get/MaybeStore on
"https://"+Host+RequestURI() instead of r.URL.String() (path-only on
server requests). Two vhosts sharing /logo.png no longer collide.
MaybeStore gains explicit cacheURL arg; all callers updated.
Add TestMediaCacheVhostIsolation: store hostA/x.png, assert hostB/x.png
→ MISS.

Fix 2 (CrowdSec self-loop): secubox-waf-ng-worker@.service --crowdsec-url
was http://127.0.0.1:8080 — the nftables DNAT VIP that fans requests back
into the workers themselves. Changed to http://10.100.0.1:8080 (LXC-bridge
LAPI, same as Python addon). Added blocking unit comment + CUTOVER.md §1.2
crowdsec-url self-loop check.

Fix 3 (body-inspect cap + audit):
- maxBodyInspect const → defaultMaxBodyInspect; Server gains maxBodyInspect
  field wired from new --max-body-inspect flag (default 1 MiB, operator-
  tunable).
- When body read returns exactly cap bytes (truncated), emit AUDIT log line
  (action=body-inspect-truncated) to threat log + stderr so truncation is
  operator-visible; request is never blocked on audit.
- Added known_gap_body_payload_after_1mib_prefix parity fixture documenting
  the prefix-bounded inspection gap with honest note.
- Added CUTOVER.md §1.6 "body inspection cap" gate with operator sign-off
  checklist and three mitigation options.

Fix 4 (seccomp/hardening): secubox-waf-ng-worker@.service was missing
SystemCallFilter, SystemCallArchitectures, ProtectKernelTunables/Modules/
Logs, ProtectControlGroups, RestrictNamespaces, LockPersonality,
RestrictSUIDSGID, RestrictRealtime, MemoryDenyWriteExecute, PrivateDevices.
Added full set matching project convention (secubox-mesh.service) as
mandated by spec §6/CSPN.

Fix 5 (trusted-host whitelist): Server gains trustedHosts map + isTrustedHost
method. --waf-skip-hosts flag (default: git.gk2.secubox.in, git.secubox.in,
admin.gk2.secubox.in, 10.100.0.1:9080) mirrors Python check_request whitelist
(secubox_waf.py:761-763). Trusted hosts bypass WAF inspection before rule
matching. Add TestTrustedHostSkipsWAF (with sanity check that untrusted host
is still blocked) and TestIsTrustedHost/TestParseTrustedHosts unit tests.
2026-06-26 15:27:10 +02:00
11438e394c docs(sbxwaf): bench harness + cutover/rollback runbook with parity-gap gates (ref #744)
- scripts/sbxwaf-bench.sh: wrk/hey bench harness against legacy mitmproxy
  (10.100.0.60:8080) and shadow sbxwaf (127.0.0.1:8081); captures req/s,
  p99, RSS; prints comparison table with PASS/FAIL for all 3 go/no-go gates
  (>5× req/s·core, p99<1/3, RSS<1/4); shellcheck clean.

- packages/secubox-waf-ng/docs/CUTOVER.md: operator runbook with 6 sections:
  pre-cutover checklist (CA, CrowdSec JWT, COMPLETE log4shell corpus,
  null-byte \x00 fix, goform FP fix, parity green), shadow-run procedure,
  go/no-go gate table, exact HAProxy server re-point + nftables DNAT topology,
  single-edit rollback, and post-cutover monitoring (threat log, cookie-audit,
  RuntimeDirectoryPreserve guarantee, CrowdSec JWT rotation constraint,
  Python WAF unescaped-Host XSS backport note, body URL-decode limitation).
2026-06-26 15:02:18 +02:00
3f24034c37 test(sbxwaf): make log4shell corpus gap loud + isolate per-fixture ban state (ref #744)
Fix 1 (medium): add known_gap fixture log4shell_jndi_corpus_gap to waf-parity-fixtures.json.
  The Go corpus (secubox-waf/config/waf-rules.json) is missing the log4shell category
  present in the Python corpus (secubox-mitmproxy/data/waf-rules.json); jndi:ldap payloads
  are missed silently by Go. The fixture emits a visible KNOWN GAP t.Logf line in CI so
  the coverage gap is never silent. expect=allow (current Go gap behaviour).

Fix 2 (low): sqli_union_all_select client_ip changed from 45.33.32.156 to 45.33.32.158.
  Two warn-fixtures previously shared IP 45.33.32.156 (sqli_union_all_select +
  scanner_sqlmap_ua), accumulating ban count to 2; a third reuse would silently flip
  verdict to ban. Each warn fixture now uses a distinct IP so per-fixture verdicts are
  independent. Comment added to parity_test.go explaining the shared-Ban lifecycle
  (ban-sequence fixtures are the only intentional cross-fixture accumulation).

Result: 52 fixtures, 52 pass, 3 known-gap lines visible, go build clean.
2026-06-26 14:54:17 +02:00
7814bee861 test(sbxwaf): decision parity harness vs mitmproxy (ref #744)
51-fixture corpus (testdata/waf-parity-fixtures.json) covering:
- allow (6): benign GET/POST, URL-encoded body not decoded by engine
- warn (33): SQLi, XSS, LFI, RCE, scanners, honeypots, CVEs, router botnet,
  URL-encoded path+query attacks (proves unquote_plus decode)
- ban (1): 3-hit sequence for ip 198.51.100.99 reaching threshold
- skip (11): static assets, /health, NC bypass paths, RFC1918 IPs
- known_gap (2): router-goform unanchored ';' FP + RE2 null-byte CVE patterns

TestWAFParity calls the REAL decision path (privateCIDR / staticAsset /
ncBypass / Rules.Match / Ban.Record) — no re-implementation in the test.
Findings during harness construction:
- router-goform FP: shared Python+Go pattern bug (';' in common UAs), NOT
  a parity regression — documented as known_gap
- Log4Shell gap: secubox-waf/config/waf-rules.json (Go corpus) is missing
  the log4shell category present in the Python mitmproxy rules — corpus gap
- Body URL-decoding asymmetry: Rules.Match does not decode body (only
  path+query); documented with two fixture variants
- 5 null-byte RE2 patterns skipped at compile: cve-ast-2022-42706,
  cve-ast-2023-37457, cve-opensips-2023-49323, cve-prosody-2022-0217,
  cve-strophe-2022-29168 — documented as known_gap

go test ./cmd/sbxwaf/ -run TestWAFParity -v: 51 pass, 0 fail
go build ./...: clean
2026-06-26 14:45:13 +02:00
16a4e6e63d fix(packaging): scope sbxwaf sandbox to WAF-owned leaf dirs (ref #744)
M1: move --threat-log default from /var/log/secubox/waf-threats.log to
/var/log/secubox/waf/waf-threats.log (WAF-owned leaf); update ExecStart
and postinst to create the leaf dir (secubox-waf:secubox-waf 0750);
narrow ReadWritePaths from /var/log/secubox to leaf dirs only
(/var/log/secubox/waf /var/log/secubox/cookie-audit).

L1: fix AppArmor profile — /var/log/secubox/ parent entry changed from
rw to r (traverse only); add explicit rw entries for both leaf dirs
(/var/log/secubox/waf/** and /var/log/secubox/cookie-audit/**).
2026-06-26 14:31:35 +02:00
e275f730ec feat(packaging): secubox-waf-ng deb + hardened systemd worker + AppArmor (ref #744)
packages/secubox-waf-ng/:
- debian/control: Architecture: arm64, Standards-Version: 4.6.2, compat 13
- debian/rules: cross-build sbxwaf from secubox-toolbox-ng Go module
  (GOOS=linux GOARCH=arm64 CGO_ENABLED=0 -mod=vendor, execute_after_dh_auto_install)
- systemd/secubox-waf-ng-worker@.service: User=secubox-waf,
  RuntimeDirectory=secubox + RuntimeDirectoryPreserve=yes (#741 socket-wipe fix),
  NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
  CapabilityBoundingSet= (drop all), RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX,
  MemoryMax=256M, listens on 127.0.0.1:808%i (instances 1+2)
- debian/secubox-waf-ng.apparmor: enforce profile for /usr/sbin/sbxwaf
  (rw log/cache/run, r config/secrets, deny-all else)
- debian/postinst: adduser secubox-waf --system --group, leaf dirs only
  (NEVER chmod shared parents /etc/secubox /var/log/secubox /var/cache/secubox
  to 0750 — traversal constraint from #511/#620), aa-enforce, systemctl enable+start @1+2
- debian/prerm: stop+disable @1+2 workers

Build: dpkg-buildpackage -a arm64 -us -uc -b -d produced
secubox-waf-ng_1.0.0-1~bookworm1_arm64.deb (2.0M, stripped arm64 binary 5.5M)
containing /usr/sbin/sbxwaf, /lib/systemd/system/secubox-waf-ng-worker@.service,
/etc/apparmor.d/usr.sbin.sbxwaf.
2026-06-26 14:25:01 +02:00
49edf6670a fix(sbxwaf): HTML-escape Host in error pages — prevent reflected XSS (ref #744)
errorPage(code, host) was substituting r.Host verbatim into the 502/504
templates, allowing an attacker to inject arbitrary HTML via a crafted
Host header. Apply html.EscapeString before substitution.

Add TestErrorPageEscapesHost (asserts raw payload absent + escaped form
present) and TestErrorPageSubstitutesHostNormal (safe hosts unchanged).
2026-06-26 14:15:57 +02:00
ae930c0347 feat(sbxwaf): synthetic themed error pages on upstream failure (ref #744)
Port the Python secubox_waf.py error() hook (~line 1096) to Go:
- 502 Bad Gateway    — connection refused/dial failure → error-502.html
- 503 Svc Unavail    — all other errors               → error-503.html
- 504 Gateway Timeout — net.Error.Timeout()           → error-504.html

Templates embedded via //go:embed (templates/*.html), verbatim copies
from Python ERROR_502_PAGE / ERROR_503_PAGE; error-504.html is 502 with
"502"→"504" and "Bad Gateway"→"Gateway Timeout" (mirrors Python in-place
replace). {host} and {time} placeholders substituted at request time.

upstreamErrorCode() helper maps net.Error → 502/503/504. ErrorHandler wired
in both the fallback proxy path (main.go) and cached proxy path (routes.go).

TDD: 5 new tests (errorPage substitution, all codes, unknown-code fallback,
themed 502 on dead backend, 504 on timeout). Full suite green (2.2s).
2026-06-26 14:09:51 +02:00
c3940a2958 fix(sbxwaf): media-cache Flusher passthrough + oversize handler test (ref #744)
- Fix I2: implement http.Flusher on cachingResponseWriter so
  httputil.ReverseProxy can flush chunks incrementally to the client
  (progressive video / PeerTube streaming); pure pass-through, does not
  affect cache buffer capture.
- Fix M1: add TestMediaCacheHandlerOversizeStreamsFullBody — end-to-end
  regression guard that a >16 MiB video/mp4 response streams its FULL
  body (byte count + SHA-256 checksum) to the client and is NOT cached;
  passes against current code, race-clean.
- Fix M2: document the mtime-as-atime LRU choice at the loadIndex site
  so a future reader understands why ModTime() is used instead of atime
  (relatime suppresses atime on most Linux filesystems; mtime is set
  explicitly via os.Chtimes on every Get hit).
2026-06-26 14:04:15 +02:00
f1573c37d2 feat(sbxwaf): media-cache (16MB/obj, 2GB total) (ref #744)
Add disk-backed response media cache ported from media_cache.py:
- GET image/video/audio/font/css/js responses cached under sha256(url)
- On-disk layout: <dir>/<key[:2]>/<key> + <key>.m sidecar (body+meta)
- LRU eviction by atime when total exceeds 2 GiB cap
- TTL from max-age (or 1h default); nowFn seam for deterministic tests
- Cache hit short-circuits upstream; miss captures via cachingResponseWriter
- Oversize (>16 MiB) bodies not stored but streamed fully to client
- --media-cache-dir flag (default /var/cache/secubox/waf/media; empty = off)
- 11 TDD tests: store/get, non-media reject, oversize reject, expiry,
  handler hit, no-store skip, stats, eviction, non-GET, persistence, miss-stores
2026-06-26 13:56:57 +02:00
85b508d4f2 fix(sbxwaf): atomic cookie-audit drop counter (race-free) (ref #744) 2026-06-26 13:50:50 +02:00
f06cb2dc28 feat(sbxwaf): RGPD cookie-audit JSONL ledger (ref #744)
Add cmd/sbxwaf/cookieaudit.go: CookieAudit struct with buffered async
channel (256), single writer goroutine, non-blocking Record (drop-on-full),
SHA256-hashed values, raw Set-Cookie parsed directly for SameSite support.
Wire --cookie-audit-log flag into Server and Routes ModifyResponse.
2026-06-26 13:47:23 +02:00
a85668f39d feat(sbxwaf): CrowdSec LAPI alert bridge on ban (ref #744)
- Add cmd/sbxwaf/crowdsec.go: CrowdSecClient satisfying CrowdSecReporter;
  POSTs a LAPI /v1/alerts JSON array (ported from secubox_waf.py
  _ban_via_crowdsec) with 2s timeout, no redirect following (SSRF hygiene),
  best-effort error handling (log + return, never block/panic).
- Add cmd/sbxwaf/crowdsec_test.go: TDD — TestCrowdSecAlertPayload (httptest
  capture of POST /v1/alerts, bearer token, payload fields) +
  TestCrowdSecBestEffortOnError (dead URL, no panic).
- Wire flags in main(): --crowdsec-url, --crowdsec-jwt-file (secret read from
  file, not argv), --crowdsec-ban-duration (default 4h); wires srv.crowdsec
  when both url+jwt-file are set; leaves nil otherwise (bridge disabled).
2026-06-26 13:39:22 +02:00
64258b98d8 feat(sbxwaf): graduated WARNING/BAN responses + threat log (ref #744)
Task 3.2: wire ban.Record into the handler for graduated 403 responses:
- count < threshold → writeWarning (403, cyberpunk WARNING_PAGE port, X-SecuBox-WAF: warning)
- count >= threshold → writeBan (403, ban page, X-SecuBox-WAF: banned)
- ThreatLog appends one NDJSON line per hit to /var/log/secubox/waf-threats.log
  (O_APPEND|O_CREATE, 0640, best-effort — never crashes the request path)
- Server gains ban *Ban + threatLog *ThreatLog + crowdsec CrowdSecReporter (nil seam for Task 4.1)
- main() wires NewBan(300s,3) + NewThreatLog(--threat-log) at startup
- CrowdSecReporter interface + go crowdsec.Report() call ready for Task 4.1 to slot in
2026-06-26 13:34:02 +02:00
8ea14e660a feat(sbxwaf): sliding-window graduated ban (ref #744)
Adds cmd/sbxwaf/ban.go: Ban struct with NewBan(window, threshold) and
Record(ip, nowUnix) that prunes stale hits, counts within the window,
and returns (count, banned) — mirrors Python BAN_THRESHOLD=3/BAN_WINDOW=300s.
Map capped at 100k IPs to bound memory under flood. Tests: TDD pass 3/3.
2026-06-26 13:27:09 +02:00
4334f93edc fix(sbxwaf): forward full request body intact, cap only inspection (ref #744)
Finding 1 (data corruption): replace LimitReader-restore pattern with a
streaming MultiReader approach: read up to maxBodyInspect (1 MiB) into a
prefix buffer for WAF inspection, then restore r.Body as
io.MultiReader(bytes.NewReader(prefix), r.Body) so the upstream proxy
receives every byte intact. Large uploads (PeerTube / Nextcloud) no longer
get truncated at 1 MiB.

Finding 2 (dead code): remove healthPath() from inspect.go — it was never
called; its logic is fully covered by staticAsset().

Tests added:
- TestInspectLargeBodyForwardedIntact: POST 1 MiB + 4 KiB → backend receives
  full body byte-for-byte (regression test for the truncation bug).
- TestInspectLargeBodyAttackInFirstMiB: attack in first 1 MiB of large body
  is still blocked (streaming inspection still works).
2026-06-26 13:23:42 +02:00
02b1c7a461 feat(sbxwaf): request inspection + CIDR/static/NC skip-lists (ref #744)
Wire Rules.Match into the HTTP handler (Task 2.2):
- inspect.go: privateCIDR (RFC1918+loopback), staticAsset (13 exts + health),
  ncBypass (/index.php/login/v2/ + /ocs/v2.php/core/login), clientIP (XFF
  trusted only when peer ∈ TRUSTED_PROXIES), maxBodyInspect=1MiB constant.
- main.go: Server.rules *Rules field; handler reads body capped at 1MiB,
  restores via io.NopCloser(bytes.NewReader) before proxying; WAF hit → 403;
  Connection: close added to upstream requests (#496).
- main() wires LoadRules(*rules) when --rules flag is provided.

Ports faithfully from secubox_waf.py: _is_whitelisted/_WL_NETS (lines 28-47),
get_real_client_ip (lines 193-219), check_request fast-path (lines 764-769).

Tests: 5 new (BlocksAttack, PrivateIPBypass, StaticAssetSkip, NCBypass,
BodyForwarded) + 14 existing = 19/19 PASS.
2026-06-26 13:18:56 +02:00
efb390b713 feat(sbxwaf): regex WAF rule engine from waf-rules.json (ref #744) 2026-06-26 13:09:23 +02:00
f2bdef341c fix(sbxwaf): inject shared transport into all route proxies (ref #744)
- LoadRoutes(path, transport http.RoundTripper) — transport now required at
  load time; nil falls back to http.DefaultTransport gracefully
- buildEntries: removes the r.transport != nil guard — transport is always
  set at Routes construction, never post-hoc
- Server gains a transport http.RoundTripper field; main() constructs the
  tuned *http.Transport (dial timeout + pool settings) BEFORE LoadRoutes so
  startup-built proxies share the same pool as reload-built ones
- handler() uses s.transport when available; falls back to a local transport
  only for test Servers that don't inject one (backwards-compat)
- main(): removed the post-hoc s.routes.transport = transport assignment
- routes_test.go: adds TestRoutesInjectedTransportUsed — sentinel transport
  proves startup-built proxies use the injected transport, not DefaultTransport
- Existing TestRoutesLookup / TestRoutesLookupCaseAndPort / TestRoutesHotReload
  updated to pass nil transport (all still pass)
2026-06-26 13:03:25 +02:00
bd6b7c3ebf feat(sbxwaf): haproxy-routes.json loader + hot-reload + cached reverse-proxy (ref #744)
- New routes.go: Routes struct with RW-locked map, LoadRoutes() parses
  haproxy-routes.json ({"host": ["ip", port]}), skips malformed entries
  without panicking, Lookup() lowercases+strips-port before probe.

- Hot-reload via internal/reload.Watcher (throttle=0, mirrors policy.go):
  Target.Load re-parses file, Target.Apply atomically swaps entries map
  under mu.Lock. Routes.Maybe() called once per request in handler().

- Perf: *httputil.ReverseProxy built once per ip:port backend at load/reload
  time (sync.Map proxyCache keyed by "ip:port"), never per-request. Shared
  *http.Transport injected from Server.handler() so all backends share one
  connection pool.

- main.go: --routes flag now calls LoadRoutes, sets srv.routes + srv.routeLookup;
  handler uses ProxyFor() for cached proxy, falls back to one-off only when
  routes is nil (test injection path).

- Tests: 5/5 PASS — Lookup, case/port normalisation, hot-reload with
  os.Chtimes bump + Maybe() trigger.
2026-06-26 12:50:47 +02:00
d747b705ce feat(sbxwaf): reverse-proxy skeleton + listener (ref #744)
- cmd/sbxwaf/main.go: Server struct with routeLookup func field, handler()
  reverse-proxying via httputil.ReverseProxy, X-SecuBox-WAF: inspected stamp,
  421 for unmapped hosts, flags (--listen, --ca-cert, --ca-key, --routes,
  --rules, --upstream-timeout), lazy CA load via forge.LoadCA.
- cmd/sbxwaf/main_test.go: TestProxyPassthrough (200+body+header) and
  TestProxyUnmapped (421) — both green; go build ./... clean.
2026-06-26 12:41:51 +02:00
dacafcfdee refactor(toolbox-ng): extract internal/reload (ref #744)
Extract the mtime hot-reload pattern from cmd/sbxmitm/policy.go into a
generic, Policy-agnostic internal/reload package (Target/Watcher/StatMtime/
LoadLines). policy.go rewired to use reload.Watcher; private reloadTarget
struct, maybeReload body, statMtime, scanLines/loadLines/loadLinesRaw removed.
Policy retains its own throttle gate (reloadThrottle/reloadMu) so existing
reload_test.go field mutations compile unchanged; the Watcher runs with
throttle=0 and is gated by Policy.maybeReload().

7 new tests in internal/reload (basic, throttle, stat, strip-comments,
missing-file, multi-target, concurrent/race). All parity fixtures + reload
tests green: go test ./internal/reload/ ./cmd/sbxmitm/ -count=1 -race PASS.
2026-06-26 12:36:50 +02:00
e47cd115fd refactor(toolbox-ng): extract internal/relay (ref #744)
Move emit/emitSync/emitTimeout from cmd/sbxmitm/sidecar.go into the new
package internal/relay as Emit/EmitSync/EmitTimeout, so cmd/sbxwaf can
reuse the fire-and-forget unix-socket POST transport without duplication.

- internal/relay/relay.go: Emit (detached goroutine), EmitSync (2s timeout,
  synchronous), EmitTimeout const — pure stdlib, no new go.sum entries
- internal/relay/relay_test.go: TDD tests — unix echo server, asserts
  request-line "POST /ingest HTTP/1.1" + exact body
- cmd/sbxmitm/relay.go: relayEmit now calls relay.Emit
- cmd/sbxmitm/sidecar.go: declarations removed, retained as doc/comment file
- cmd/sbxmitm/sidecar_test.go: rewired to relay.EmitSync/relay.Emit/relay.EmitTimeout

go test ./internal/relay/ ./cmd/sbxmitm/ -count=1: PASS (both packages)
go build ./...: clean
2026-06-26 12:28:29 +02:00
18e625fd88 refactor(toolbox-ng): extract internal/httpcodec (ref #744)
Move gzip/br/zstd codec primitives from cmd/sbxmitm/gzip.go into a new
shared package internal/httpcodec (GunzipBytes, GzipBytes, UnbrotliBytes,
BrotliBytes, UnzstdBytes, ZstdBytes + Decode/Encode dispatchers).
Rewire cmd/sbxmitm/gzip.go and all three test files to call httpcodec.*.
No behaviour change; cmd/sbxmitm still builds and all tests pass.
2026-06-26 12:21:40 +02:00
8e1f8f2155 refactor(toolbox-ng): extract internal/forge from sbxmitm (ref #744)
Move CA, loadCA→LoadCA, forge→Forge, firstPEMBlock, parseKey from
cmd/sbxmitm/main.go into a new shared package internal/forge so that
the future cmd/sbxwaf can reuse them without duplication.

No behaviour change: cmd/sbxmitm wires in forge.LoadCA / ca.Forge;
ca.cert (unexported) becomes forge.CA.Cert (exported, needed by tests
and future callers). Both suites green:
  go test ./internal/forge/ ./cmd/sbxmitm/ -count=1
2026-06-26 12:12:50 +02:00
ccf6d45a08 docs(plan): WAF→Go sbxwaf implementation plan, 10 phases TDD (ref #744) 2026-06-26 12:04:25 +02:00
218a65068a docs(wiki): publish refonte 126 modules to wiki/ — MODULES + CATEGORIES ×4 langues (ref #742)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-26 11:56:20 +02:00
0bd3b9e035 Merge branch 'docs/742-wiki-refonte-snapshots-webui-determinist' 2026-06-26 11:56:05 +02:00
6ec92bd29d docs(spec): WAF→Go bench targets to >5×/p99<⅓, blocking go/no-go (ref #744) 2026-06-26 11:52:44 +02:00
c01f10c474 docs(wiki): refonte 126 modules — snapshots WebUI déterministes + READMEs 4 langues (ref #742)
Capture des 126 dashboards authentifiés (JWT minté serveur, injection localStorage)
avec attente de COMPLÉTION d'affichage déterministe (sentinelles de chargement
purgées + réseau calmé, plafond --delay) au lieu d'un sleep fixe. 126/126 OK.

- capture-screenshots.py: _wait_content_ready déterministe, mode --token (bypass
  login), goto domcontentloaded (plus de stall networkidle sur dashboards live)
- generate-docs.py: 23 modules jusqu'ici non documentés ajoutés (descriptions
  réelles depuis debian/control, EN/FR/DE/ZH), licence MIT→LicenseRef-CMSD-1.0,
  images wiki en URL raw.githubusercontent absolue (relatif = 404 sur GitHub wiki)
- 126 snapshots + thumbnails régénérés
- 126 READMEs paquet succincts + pages wiki MODULES/CATEGORIES ×4 langues

Gap modules documentés vs découverts: 23 → 0.
2026-06-26 10:14:14 +02:00
0b2094f43f docs(spec): WAF→Go sbxwaf host-native replacement design (ref #744)
Brainstorming-validated design: perf-driven complete replacement of the WAF
mitmproxy/mitmdump inspection layer by a dedicated host-native Go binary sbxwaf,
sharing an extracted core with sbxmitm. Covers architecture, component isolation,
full feature port (routing/rules/ban/CrowdSec/cookie-audit/media-cache/error
pages), host-native hardening, and shadow→parity→cutover→rollback migration.
2026-06-26 09:31:59 +02:00
165dc21842 fix(security-posture): RuntimeDirectoryPreserve=yes (last unit missing it) (#741)
The shared /run/secubox RuntimeDirectory is wiped on any sibling restart unless
every unit sets RuntimeDirectoryPreserve=yes (85/86 already did; this was the
straggler that could still trigger the board-wide socket wipe).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:37:59 +02:00
e53f6a3f46 fix(waf): feed Threats/Blocked/Protected from CrowdSec (zero-metrics) (#733)
The dashboard's Threats 24h / Blocked cards and the severity/category donuts read
the inline mitmproxy threat log (THREATS_LOG), which is normally EMPTY because
CrowdSec + the firewall bouncer do the real detection/blocking — so they showed 0
while bans (from cscli decisions) worked.

- _get_crowdsec_alerts(): cscli alerts list -o json (same source as bans), with a
  scenario-derived severity.
- _overlay_crowdsec_stats(): fold alert counts into total_threats / threats_today
  / blocked_24h + by_severity / by_category / top_countries + last_threat.
- _protected_vhost_count(): Protected = HAProxy->mitmproxy route-map size.
- Wired into the warm-loop refresh (background; cscli never blocks a request).

Verified on gk2 (1.2.7): Threats 50, Blocked 50, Protected 255, donut
crit 16 / high 13 / med 21, live last_threat — was all 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:59:06 +02:00
bda86be02a fix(threatmesh): move navbar entry into the Wall master section (#728)
ThreatMesh was in an ad-hoc 'security' category — not one of the 6 master
sections (auth/boot/mesh/mind/root/wall) — so it sat alone in an unsanctioned
section. It's sovereign threat-intel + blocklist enforcement, so it belongs in
WALL alongside CrowdSec / IP Blocklist / CyberFeed / Threats (order 706). The
'security' section is now empty/removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:49:21 +02:00
220f996928 feat(mediaflow): reconstruct multi-part HLS streams for full clone (#736)
video.twimg.com (and HLS generally) is fetched as hundreds of .m4s/.ts segments
+ a few .m3u8 playlists. That flooded Discovered Media with useless fragments and
none was cloneable on its own.

- _collapse_streams: group every multi-part stream to ONE entry (Twitter by media
  id; generic HLS by segment directory). Reconstruct the fully-cloneable URL —
  the master .m3u8 (video+audio) when seen, else the highest-res video variant +
  an audio variant. Collapse happens at ingest so the segment flood never reaches
  the durable store; part counts take max (no inflation).
- clone: audio_url support → ffmpeg muxes separate video+audio variant playlists
  (-map 0✌️0 -map 1🅰️0) when there's no master.
- UI: kind=stream with a "N parts (+audio)" count; Clone passes url+audio_url via
  an index lookup (no escaping pitfalls). Discovered URLs already clickable.

Verified on gk2 (2.2.0): ~1800 twimg segments collapsed to one entry per video;
cloning a stream's master playlist produced a complete 720x1280 h264 + aac mp4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:21:05 +02:00
6a34dee598 feat(mediaflow): make Discovered Media URLs clickable (#736)
Render the discovered media URL as a new-tab link (rel=noopener) instead of
plain truncated text, so the operator can open the manifest/watch URL directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:12:23 +02:00
b6db15ca03 docs: HISTORY for R4 recos + catch-log ownership fix (#736)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:15:39 +02:00
75241d0774 feat(mediaflow): persist Discovered Media + package yt-dlp refresh (#736)
- _read_catch merges the tmpfs catch log into a durable capped store
  (discovered.json, 2000 max) — discovery survives reboots; idempotent override
  merge (no hit inflation). Surfaces kind=page (YouTube watch URLs).
- control Recommends ffmpeg/yt-dlp/curl; ship secubox-yt-dlp-refresh + a weekly
  timer that pulls the latest standalone yt-dlp to /usr/local/bin (apt's is months
  behind and can't self-update; YouTube extractors change often). postinst creates
  the library dir + kicks one refresh.

Verified on gk2 (2.1.1): durable store holds YouTube watch pages + manifests;
yt-dlp refreshed to 2026.06.09 (parses YouTube); timer active.
2026-06-24 17:14:50 +02:00
7f1b1727d4 feat(catcher): capture YouTube watch pages + fix catch-log ownership (#736)
- mediacatch.go: also record cloneable video PAGES (YouTube watch/shorts/live,
  youtu.be) as kind=page — the watch URL is what yt-dlp clones; signed chunk URLs
  aren't reusable. Pages dedup by full URL so each video id is kept distinct.
- tmpfiles.d: pre-create /run/secubox/media-catch.jsonl owned by the writer
  (secubox-toolbox). A stale secubox-owned file in the sticky /run dir was making
  the worker's O_APPEND fail silently -> catcher disabled itself -> nothing
  captured. Now correctly owned every boot; mediaflow reads it as other (0644).

Verified on gk2 (0.1.22): YouTube watch URLs captured as kind=page, distinct ids.
2026-06-24 17:14:50 +02:00
25e41c1fba feat(toolbox): R4 analyst tier in the banner level switch (#736)
Add R4 to the inline banner topbar (R0..R4) + /__toolbox/set-level + the by-MAC
change-level validation + the 7d level-distribution analytics buckets. R4 is the
deepest analyst / media-reverse-catcher tier, gated to the wg path like R3.
Functionally the box already runs MITM-everything by default (Phase 1-2); this is
the selectable affordance.

Verified on gk2: R4 button renders in the served banner; set-level returns
{"ok":true,"level":"r4"} and the bundle reads it back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:47:57 +02:00
c709c3e9df docs: HISTORY for R4 analyst mode + media reverse-catcher (#736)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:42:48 +02:00
8fd451f6ba feat(mediaflow): R4 Discovered Media + Clone library (#736)
New dashboard cards driven by the sbxmitm media reverse-catcher:
- /discovered reads /run/secubox/media-catch.jsonl (caught media URLs, grouped,
  newest first) + reports the active downloader.
- /clone enqueues a download (yt-dlp if present, else ffmpeg stream-copy for
  HLS/DASH manifests + direct media) into a durable library; single async
  worker, 30-min cap, lazily (re)started per-request so it runs under the
  aggregator too (no sub-app lifespan).
- /library + /clone/jobs list jobs; /download/{id} serves the file; DELETE
  removes it.
- Frontend: "Discovered Media" (Clone buttons) + "Cloned Library" (Download/
  delete) cards.

Verified end-to-end on gk2 (mediaflow 2.1.0): caught an HLS manifest → cloned via
ffmpeg → 464 MiB mp4 in the library. yt-dlp installed for broader site support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:41:23 +02:00
44c751154c feat(R4): MITM-everything splice doctrine + sbxmitm media reverse-catcher (#736)
R4 / analyst mode foundation — performance is explicitly secondary to visibility:

- tls-splice-seed.conf: flip doctrine from "splice media CDNs for speed" to
  "MITM everything; splice ONLY hosts that genuinely break under MITM (cert
  pinning)". Seed reduced to api.anthropic.com. The banner now reaches every
  HTML page; the catcher can see media URLs. (Live: learned splices cleared;
  splice autolearn already gated off via tls_splice=off.)
- sbxmitm media reverse-catcher (mediacatch.go): on 2xx MITM'd flows, record
  cloneable media URLs (HLS/DASH manifests + direct audio/video + googlevideo
  videoplayback) to /run/secubox/media-catch.jsonl — URLs only, never bodies,
  deduped per worker, atomic appends, best-effort/fail-open. Gated --media-catch
  (default on).
- worker unit: ReadWritePaths=/run/secubox so the catcher can write under
  ProtectSystem=strict.

Verified on gk2 (toolbox-ng 0.1.20): captured an HLS manifest + a direct MP4
through a worker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:32:43 +02:00
e33a19d38e fix(mediaflow): Top Media Services from DPI cumulative store (#728)
/exfil is live-window only (~60s), so the services table blinked empty whenever
nothing was streaming that instant. _media_services_list() now reads the DPI
7-day cumulative store (/var/lib/secubox/dpi/cumulative.json, same schema) so the
ranking persists; cards + Active Streams stay live (active-now). Falls back to
live exfil if cumulative is absent. Bandwidth cell = cumulative total transferred.

Verified on gk2 (mediaflow 2.0.3): Top Media Services shows YouTube 33.3 MB.
Note: dashboard routes via the aggregator (in-process import) — restart
secubox-aggregator to pick up mediaflow code changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:21:23 +02:00
220bdd6c1f fix(mediaflow): restore dashboard — align API with frontend contract (#728)
The 2.0.x DPI-exfil rewrite changed the response shapes the dashboard reads, so
every card/table fell back to 0 even when media flows were present:

- /status now returns video_streams / audio_streams / bandwidth_mbps /
  active_clients (the frontend's card fields), alongside the diagnostics.
- add the missing /streams endpoint → {streams:[{client_ip,type,service,
  bandwidth,duration}]} (frontend called /streams; backend only had
  /get_active_streams).
- /services now returns {services:[...]} with streams/bandwidth/percent cells
  (was a bare array → frontend's `.services` was undefined). Refactored into
  _media_services_list() so /services/by-category keeps working.
- split media flows into video/audio (service name + host hint) and derive Mbps
  from the DPI capture window (SECUBOX_DPI_WINDOW, 60s).
- add missing `import os`.

Verified on gk2 (mediaflow 2.0.2): endpoints return the correct shape; cards
read 0 only when no media is streaming (exfil media-category empty that instant).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:13:01 +02:00
d476b2fb18 docs: HISTORY for nonce-CSP banner + Claude API splice + youtube unblock (#728, #735)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:59:51 +02:00
49a6fb5af8 fix(sbxmitm): banner runs on nonce-CSP sites + splice Claude API (#728)
The transparency banner is inlined (service-worker-proof), but on nonce-based
CSP sites (YouTube, most news) a nonce/hash makes 'unsafe-inline' IGNORED, so the
bare inline <script> was silently blocked and the banner never appeared.

- csp.go: relaxCSPForLoader now BORROWS the page's own nonce and returns it; the
  injected <script nonce=…> is stamped with it (surgical — the page's CSP, nonces
  and hashes stay intact). Falls back to forcing 'unsafe-inline' (dropping
  nonce/hash/strict-dynamic) only when there is no nonce to borrow. Nonce values
  are validated to the base64 charset to prevent attribute breakout.
- banner.go/gzip.go/main.go: thread the borrowed nonce through injectIntoBody →
  injectHTML → injectInlineBanner onto the <script> tag.
- Keeps the earlier Trusted-Types drop (0.1.17).
- tls-splice-seed.conf: splice api.anthropic.com — cert-pinned Claude API/SDK
  clients reject the MITM CA; pass them through (claude.ai web stays MITM'd).

Tests rewritten for inline semantics + nonce borrow/validation; all pass.
Deployed gk2 toolbox-ng 0.1.18, R3 workers 1-4. Verified end-to-end on YouTube
(200, banner nonce == page nonce) and lemonde/lefigaro (unsafe-inline fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:58:33 +02:00
c30680fcf3 docs: HISTORY entry for YouTube bannering Trusted Types fix (#728)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:15:31 +02:00
361ceb8393 fix(sbxmitm): strip Trusted Types CSP so the transparency banner mounts on YouTube (#728)
YouTube (and other strict-CSP sites) send `require-trusted-types-for 'script'`,
which blocks the R3 transparency banner's DOM injection even when the loader
script is allowed to run — the banner silently never appeared.

relaxCSPForLoader now drops the `require-trusted-types-for` and `trusted-types`
directives alongside the existing script-src relax, and omits the resulting
empty CSP header line (YouTube's TT directive is a standalone header). Covered
by a local Go unit test; deployed to gk2 (toolbox-ng 0.1.17, R3 workers 1-4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:11:54 +02:00
4f8eb711f3 fix(mediaflow): consume DPI public /exfil instead of dead netifyd /flows (2.0.1)
Some checks failed
License Headers / check (push) Has been cancelled
mediaflow's internal DPI calls were unauthenticated against auth-gated /status,
/flows (netifyd, now dead) -> 401 -> empty/error dashboard. Rewired /status,
/services, /clients, /get_active_streams, /get_service_details, /summary + the
monitor task to read the public category-tagged /exfil and filter category=media.
2026-06-24 14:34:21 +02:00
4cf7c85191 docs(wiki): ThreatMesh FR page + poster, cross-linked EN/FR (#728)
French translation of the ThreatMesh explainer with the FR hero poster
(threatmesh-poster-fr.png, 1024x1536); EN<->FR language links + sidebar FR link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:22:00 +02:00
6286b83bda docs(wiki): add ThreatMesh hero poster image (#728)
Neighborhood-watch poster (1024x1536) for the ThreatMesh wiki page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:17:19 +02:00
6907b95d11 docs(wiki): ThreatMesh page — sovereign threat-intel poster + simple explainer (#728)
Neighborhood-watch explainer (free feeds + mesh, no CAPI) mirroring the
Anti-Track poster page; sidebar entry under MIND; images/README updated.
Poster art expected at wiki/images/threatmesh-poster.png.
2026-06-24 14:14:34 +02:00
d6eaf52ce1 fix(blacklist-sync): confidence gate for sovereign feed enforcement (#728)
Enforce a threat_intel IP only if corroborated by >=2 sources OR from a curated
high-trust feed (weight>=80) — avoids arming ~45k noisy single-source feed IPs.
CrowdSec local decisions + DNS-guard always enforced. Env-tunable
(SECUBOX_BL_MIN_CONSENSUS/MIN_WEIGHT). Live: ~1907 v4 + 1091 v6 enforced.
2026-06-24 14:05:55 +02:00
0a05bed028 Merge feature/728 — secubox-threatmesh (sovereign threat-intel, CAPI replacement) 2026-06-24 14:00:04 +02:00
fdfc404818 feat(threatmesh): sovereign threat-intel — feeds + mesh + API, drop CrowdSec CAPI (#728)
Phase 0 (board): CrowdSec online_client/CAPI disabled, LAPI kept.
Phase 1: secubox-threatfeed timer pulls 8 free public blocklists
  (feodo/sslbl/firehol/spamhaus-drop/blocklist.de/cins/et/dshield) ->
  shared threat_intel -> secubox-blacklist-sync -> nft. ~45k IOCs live.
Phase 2: secubox-threatmesh service gossips locally-detected CrowdSec decisions
  to SecuBox P2P peers over WireGuard + ingests peer decisions (mesh:<node>,
  consensus-counted); :8780 locked to wg*/lo by an nft drop-in.
Phase 3: /status /peers /decisions (bouncer-compatible aggregate) /mesh/ingest
  + C3BOX dashboard (sovereign mode). No CAPI, no account, no paywall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:59:59 +02:00
0fc5871169 fix(vhost): full public-FQDN vhost list with working https links (1.1.1)
list_vhosts used the nginx config FILENAME stem as the domain (arm, lyrion) and
never set url -> the dashboard showed short names with broken/None links. Now it
resolves the real public FQDN (nginx server_name, falling back to the HAProxy
route map), sets https:// URLs + correct ssl (HAProxy terminates TLS), and
appends HAProxy public routes with no nginx config. Live list: 262 vhosts, full
clickable links, 0 broken.
2026-06-24 13:28:47 +02:00
36cfb72e41 Merge feature/727 — aggregator auto-heal watchdog 2026-06-24 12:32:59 +02:00
6e62c0166d feat(aggregator): packaged auto-heal watchdog timer (#727)
Ships secubox-aggregator-watchdog.{sh,service,timer}: probes aggregator.sock
/api/v1/hub/public/menu every 2min, restarts secubox-aggregator after 2
consecutive failures (the hub/auth/menu SPOF wedged under a load spike in the
2026-06-24 incident). State file kept in /run (root-owned), NOT the shared
sticky /run/secubox — a stale secubox-owned file there can't be overwritten by
CAP_DAC_OVERRIDE-less root, which would freeze the streak and stop it triggering.
Enabled in postinst (respects masking). Verified live: state persists root:root,
timer active. Bump 0.2.3.
2026-06-24 12:32:55 +02:00
ff6fd7632f Merge feature/726 — secubox-podcaster (subscribe/download/relay, portal, audiobook ZIP, auto-download) 2026-06-24 12:15:40 +02:00
0566672615 feat(podcaster): auto-download to keep portal/share feed synced (#726)
Feeds can auto-queue new episodes (auto_dl). New feeds default on (UI checkbox);
per-feed toggle POST /feeds/{id}/autodl +  in admin; the periodic refresher
auto-queues newly published episodes, bounded by keep_per_feed. Bump 1.0.3.
2026-06-24 12:15:36 +02:00
9f5bec6a87 feat(podcaster/portal): per-episode download + per-feed ZIP download (#726)
Public portal: ⬇ per-episode mp3 download + 'Download all (ZIP)' per feed via
new public GET /public/feed/{id}/zip (STORED zip to temp, streamed, cleaned up);
public/library now exposes feed_id. Bump 1.0.2.
2026-06-24 10:53:52 +02:00
560b8d8213 feat(podcaster): Hub navbar + sbx_token auth, public portal, audiobook ZIP import (#726)
- admin UI: shared /shared/sidebar.js navbar + correct sbx_token auth
  (401->/login.html) — fixes missing navbar + false login prompt
- public listener PORTAL at /podcaster/portal/ (no auth) + GET /public/library
- audiobook ZIP import: POST /audiobook/upload (raw body, streamed to temp,
  extracts audio tracks -> synthetic feed, published in library + share feed)
- bump 1.0.1
2026-06-24 10:45:58 +02:00
7da61e8fd5 fix(podcaster): use python3 -m uvicorn + drop proxy directives dup'd by snippet (#726)
- /usr/bin/uvicorn doesn't exist on the board (it's /usr/local/bin); use the
  robust /usr/bin/python3 -m uvicorn form (status=203/EXEC fix).
- secubox-proxy.conf already sets proxy_buffering off + proxy_read_timeout;
  remove the duplicates (nginx -t emerg 'directive is duplicate').
2026-06-24 10:26:26 +02:00
f839c9260e feat(podcaster): new module — subscribe/download/relay podcasts (#726)
secubox-podcaster v1: FastAPI on /run/secubox/podcaster.sock, SQLite store,
pure-stdlib RSS/OPML parsing, asyncio+httpx download queue with progress,
generated shareable RSS (/share/feed.xml, LAN or public via secubox-exposure),
in-UI service status + TOML config, C3BOX WebUI with inline player. nginx route
shipped to the active secubox-routes.d/ include; never touches the shared
/run/secubox parent (#494). Lyrion link deferred (standalone first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:24:16 +02:00
39d7002b7a docs: record T0 build+deploy (core 1.1.10/toolbox 2.7.18 + #421 dirs-guard /run self-heal)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-24 10:12:49 +02:00
b985db14ff fix(core/#421): dirs-guard re-asserts /run/secubox 1777 root:root (RuntimeDirectory churn)
90+ services declare RuntimeDirectory=secubox, so systemd re-chowns the shared
socket parent to secubox:secubox 0755 on each start, locking out non-secubox
socket creators. Removing explicit chowns (#494) wasn't enough; the per-minute
dirs-guard now self-heals /run/secubox centrally instead of editing 90+ units.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:04:31 +02:00
1001d24180 fix(#494): stop ALL units chowning the shared /run/secubox parent (systemic)
/run/secubox parent to a module user, clobbering the canonical tmpfiles rule
(1777 root root) and 502'ing cross-user socket traversal:

- secubox-hub.service ExecStartPre (the active cascading culprit — fired on
  every (re)start, right after secubox-runtime re-applied 1777 root root)
- secubox-eye-remote / metrics / metablogizer postinsts (chown parent)
- secubox-eye-square postinst (chowned BOTH /run/secubox AND /var/log/secubox
  to secubox-eye-square — worst, #511 class)
- secubox-p2p postinst (chown parent + /var/log/secubox)

All now keep only mkdir as a fallback; the 1777 sticky parent lets each daemon
create its own socket, and module logs go to own subdirs (eye-square/p2p) instead
of owning the shared /var/log/secubox. Parent lifecycle is owned solely by
tmpfiles.d + secubox-runtime.service. Bumped: hub 1.4.4, eye-remote 1.0.1,
eye-square 1.0.4, metablogizer 1.2.2, metrics 1.0.4, p2p 1.7.1 (core 1.1.7
in prior commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 68d98bf1eb)
2026-06-24 09:59:11 +02:00
039824dcfa fix(core): stop secubox-core.service clobbering /run/secubox perms (ref #494)
secubox-core.service ran After=network.target, so its ExecStart
'chown secubox:secubox /run/secubox + chmod 775' fired LAST — after
secubox-runtime.service had re-applied the canonical tmpfiles rule
'd /run/secubox 1777 root root'. Result: the socket dir ended up
secubox:secubox, non-secubox daemons lost parent +x, and their sockets 502'd.

- Remove the chown/chmod of /run/secubox from ExecStart; the dir is now owned
  solely by tmpfiles.d (1777 root root) + secubox-runtime.service. This unit
  keeps only the www-data group membership.
- postinst defensively removes a stale non-dpkg-owned /etc/tmpfiles.d/secubox.conf
  declaring /run/secubox as 0775 secubox secubox (it overrode /usr/lib canonical).
- Ensure /var/log/suricata exists (0755) so ReadWritePaths services don't
  NAMESPACE-fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 60f05992ee)
2026-06-24 09:58:17 +02:00
4674b6023a Merge feature/65-fix-nginx-add-missing-api-routes-to-webu 2026-06-24 09:55:41 +02:00
1f21c59c19 Merge fix/53-wazuh-uvicorn-process-causes-100-cpu-spi 2026-06-24 09:55:41 +02:00
b41032107a Merge fix/121-metablog-ingest-site-dirs-should-be-crea 2026-06-24 09:55:41 +02:00
6739e19fea docs: #494 systemic fix done (7 pkgs, live-verified); #471 resolved; /var/log shared-owner follow-up
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:52:32 +02:00
b2891ff4d2 docs: #65 resolved (prod uses secubox-routes.d; template synced)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:38:57 +02:00
dba06bc47a fix(nginx): sync webui.conf template to the secubox-routes.d include (ref #65)
#65's symptom (new modules' /api/v1/<m>/ return 404 because their nginx routes
aren't loaded) is already solved in the deployed webui.conf via
'include /etc/nginx/secubox-routes.d/*.conf' — every module package drops a
location-only snippet there at install. The repo template common/nginx/webui.conf
was stale (hardcoded core blocks only, no include), which is misleading. Add the
active include so the template matches production; keep the hardcoded
crowdsec/waf/system blocks (those core packages ship only the legacy secubox.d/
snippet, so no duplicate-location).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:38:28 +02:00
b24d5bbbe0 docs: #53 + #121 fixes pushed (branches ready), update T0 status
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:34:45 +02:00
bb499b7fd5 fix(wazuh): gate service on /var/ossec to stop 100% CPU spin (ref #53)
The Wazuh API uvicorn worker was started unconditionally, but SecuBox's
documented IDS/IPS stack is Suricata + CrowdSec and /var/ossec is absent on
normal boards — so it busy-looped at ~100% CPU for nothing (operators had to
mask it by hand). Add ConditionPathExists=/var/ossec/etc/ossec.conf so systemd
reports 'inactive (condition failed)' and never starts it unless a local Wazuh
agent/manager is present, plus RestartSec=5 as a hot-respawn guard. Module
kept for opt-in SIEM integration, not removed. Verified on gk2: /var/ossec
absent, so the gated unit stays inert without needing the manual mask.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:34:11 +02:00
845683c9c9 fix(metablog): chown site dirs to secubox:secubox after ingest (ref #121)
ingest_site() runs git init/commit inside /srv/metablogizer/sites/<name> over
ssh as root, so a freshly created .git is owned root:root — but the
metablogizer service runs as 'secubox' and cannot write the repo (blocked
sub-E #113 webhook deploys). Add a fix_perms helper that chown -R
secubox:secubox the site dir after every .git-touching ingest path
(ingested-fresh x2 + ingested-with-history). Matches module postinst pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:30:13 +02:00
04064a9fb7 docs: close #468 (traversal source+live OK) + T0 live recon notes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:26:32 +02:00
a273cb570a docs: close #515/#516/#519 (verified; #519 enforcement plane repaired) + perf follow-up note
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:20:15 +02:00
73414e7550 fix(toolbox/blacklist-sync): make #519/#522 enforcement plane functional
The DNS-guard sync (Phase 13.B) aborted on the first unresolvable blocklisted
domain (getent exits 2 on NXDOMAIN; set -euo pipefail propagated it from the
ips=$(...) assignment) and, even guarded, a full sweep (~3min) overran the
unit's 120s TimeoutStartSec. Net effect: the oneshot failed every run, the nft
blacklist_v4/v6 sets stayed empty, and the protection enforcement plane was
inert. Guard the substitution with || true, raise TimeoutStartSec 120->600,
drop per-lookup timeout 2s->1s. Verified live on gk2: systemd run green, sets
populate (v4=1712, v6=273). Enforcement stays default-off (service disabled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:18:54 +02:00
6c55a21df5 docs: prioritized backlog index (T0..T6) — review of 64 open issues
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:03:04 +02:00
6e4ef4d557 docs: close #486 (geoip/ASN/flags in reports — delivered on master), clean stale worktree
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:58:20 +02:00
87a31fba45 docs: close validated lot #475/#495/#502/#507/#508/#531 (triage)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:54:37 +02:00
c8d0e7d352 docs: issue triage 2026-06-22 — sync open issues to WIP (status review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:31:22 +02:00
CyberMind
3fcdb8bd9a
Merge pull request #725 from CyberMind-FR/feature/724-toolbox-banner-in-banner-r0-r3-level-swi
toolbox banner: in-banner R0..R3 level switch (closes #724)
2026-06-24 07:51:05 +02:00
cc478872ae feat(toolbox): in-banner R0..R3 level switch — show real state + change it (closes #724)
The injected transparency banner showed the client level as static text. It's now
an interactive R0/R1/R2/R3 switch: the current tier is highlighted and a tap
changes it.

- bundle.py: lvlSwitch() renders the 4 buttons (current highlighted); wireLevels()
  fires GET /__toolbox/set-level?mh=&level= then reloads so the new tier applies.
  Dismiss-button selector tightened to button[aria-label=dismiss] (the level
  buttons are now first). invalidate() drops the cached bundle on change.
- api.py: GET /__toolbox/set-level (unauth, like /bundle) → store.set_client_level
  by hash; validates level, gates r2 by config + r3 by wg server.pubkey; invalidates
  the bundle cache.
- store.py: set_client_level(mac_hash, level) — by-hash setter (R3 wg peers have no
  captive MAC/ip, so /change-level's nft path doesn't apply).
- sbxmitm/banner.go: intercept /__toolbox/set-level and reverse-proxy to the portal
  (same path as loader.js/bundle), so the banner's same-origin fetch works on any site.

Verified live on gk2: set-level r2→bundle r2, switch back r1, bad level→400, and the
worker reverse-proxies /__toolbox/set-level on a forged origin → {"ok":true}.
2026-06-24 07:50:58 +02:00
20dfad720c docs: lock APK #685/#686 decision — non-root only, in-app VpnService plan (deferred)
Some checks failed
License Headers / check (push) Has been cancelled
2026-06-22 17:28:13 +02:00
c8fd83a80d docs: record nDPId decision — dropped for perf, ndpiReader kept (Phase 3 close) 2026-06-22 17:17:29 +02:00
dac89d9e1c revert(ndpid): drop nDPId daemon path — keep ndpiReader (perf on saturated board)
Decision (operator): nDPId is a permanent capture daemon + nDPIsrvd process →
continuous CPU/RAM on an already-saturated board (load ~4.6/4 cores), whereas the
ndpiReader producer runs in bounded niced 60s windows (~1% CPU) and frees the
core between passes. The richer-JSON / no-respawn gain doesn't justify the perf
risk, and the QEMU cross-build failed first try (fragile path). The exfil
pipeline was never cut over, so live is unaffected.

Reverts #722/#723: removes .github/workflows/build-ndpid.yml and restores
secubox-ndpid to its prior arch:all stub. DPI Phase 3 = ASN (#719) + history/
timeline (#721); nDPId dropped. ndpiReader remains the producer.
2026-06-22 17:16:48 +02:00
a0cb78cf28 docs: nDPId CI build + packaging shipped (#723), build running 2026-06-22 13:37:15 +02:00
CyberMind
8d3e977f04
Merge pull request #723 from CyberMind-FR/feature/722-dpi-phase-3-ndpid-daemon-ci-cross-build
dpi Phase 3: nDPId daemon CI cross-build (QEMU arm64) + package wiring (ref #722)
2026-06-22 13:36:04 +02:00
26b7d7a080 build(ndpid): Phase 3 nDPId daemon CI cross-build + package wiring (ref #722)
nDPId is a C daemon needing libnDPI >= 5.0 (bookworm ships 4.2) and the sandbox
cross-toolchain can't link C, so:
- .github/workflows/build-ndpid.yml — QEMU arm64 NATIVE build inside
  debian:bookworm (cmake -DBUILD_NDPI=ON bundles the right libnDPI), commits
  nDPId + nDPIsrvd to packages/secubox-ndpid/bin/ + uploads an artifact.
  Manual (workflow_dispatch), ndpid_ref input to bump.
- secubox-ndpid: Architecture all → arm64, Depends libpcap0.8 (dropped the
  external ndpid|ndpi-reader dep), debian/rules ships bin/nDPId + bin/nDPIsrvd to
  /usr/sbin (guarded so it builds before CI populates bin/). 1.1.0.

Follow-on (after CI yields a validated binary): daemon service capturing
wg-toolbox → JSON socket, and secubox-dpi-flowcap consuming nDPId JSON instead of
the ndpiReader CSV (PoC stays as fallback).
2026-06-22 13:35:57 +02:00
e2e51daca5 docs: tick DPI Phase 3 history+timeline (#721, secubox-dpi 1.1.4) 2026-06-22 13:21:16 +02:00
CyberMind
cdbe51dcb2
Merge pull request #721 from CyberMind-FR/feature/720-dpi-phase-3-per-device-daily-history-tim
dpi Phase 3: per-device daily history + timeline (closes #720)
2026-06-22 13:19:45 +02:00
9c02d0c996 feat(dpi): Phase 3 per-device daily history + timeline (closes #720)
Adds a time dimension to the DPI engine:
- collector: updateHistory() adds each window's per-device increments to today's
  daily bucket in history.json (14d retention, exact — no double counting). JSON-
  backed (the static collector has no CGO SQLite driver); richer SQL can layer later.
- api: GET /api/v1/dpi/history (?device=… per-device, else board-wide daily totals).
- dashboard: "📈 Timeline — flux/jour (14 j)" panel (daily bars, red = alert day).

secubox-dpi 1.1.4. Live on gk2: history.json accumulates today's bucket
(e6b6ca13… 114 flux); /history serves the timeline.
2026-06-22 13:19:38 +02:00
9b1036872e docs: tick DPI Phase 3 ASN enrichment (#719, secubox-dpi 1.1.3) 2026-06-22 13:12:10 +02:00
CyberMind
3260b3e520
Merge pull request #719 from CyberMind-FR/feature/718-dpi-phase-3-asn-enrichment-classify-sni
dpi Phase 3: ASN enrichment — classify SNI-less flows by destination ASN (closes #718)
2026-06-22 13:11:17 +02:00
a74dac2066 feat(dpi): Phase 3 ASN enrichment — classify SNI-less flows by destination ASN (closes #718)
~half of R3 flows have no SNI (IP-only / QUIC) and were left unclassified, so
uncontrolled cloud egress without a hostname slipped through. The collector now
falls back to the destination ASN (GeoLite2-ASN) when there is no SNI category:
if the ASN org is a known cloud/hosting provider (Amazon/Google/Cloudflare/OVH/
Hetzner/Azure/…), the flow is tagged category=cloud (service=ASN org), so
exfil_volume / new_cloud fire on it too.

- collector: asnOrg() via vendored github.com/oschwald/maxminddb-golang; opened
  once at startup, fail-soft if /var/lib/GeoIP/GeoLite2-ASN.mmdb is absent.
- debian/rules: build -mod=vendor (offline, GOPROXY=off). secubox-dpi 1.1.3.

Verified live on gk2: IP-only 52.94.236.248 → Amazon (exfil_volume), QUIC
142.250.178.142 → Google (exfil_volume), 1.1.1.1 → Cloudflare (new_cloud).
2026-06-22 13:11:09 +02:00
6cc3546dc2 docs: tracking + wiki for DPI exfil engine + Netrunner report (2026-06-22)
HISTORY/WIP/TODO checkpoint for the session (#687 DPI pipeline, #707 Netrunner
report HTML+PDF, #689/#697 sbxmitm fixes; all merged & live). New wiki page
docs/wiki/DPI-Exfiltration.md + sidebar links (kbin ToolBoX, DPI Exfil + Report).
2026-06-22 13:02:19 +02:00
CyberMind
189ad32c4c
Merge pull request #717 from CyberMind-FR/feature/716-kbin-pdf-donut-grid-layout-broken-each-d
PDF: fix donut grid layout (one combined image) — no more blank pages (closes #716)
2026-06-22 12:52:02 +02:00
1f82263d74 fix(toolbox): PDF donut grid layout — one combined image, no per-line page breaks (closes #716)
The device donut grid used absolute fpdf2 positioning + per-segment legend cells;
when it started low on a page, auto-page-break fired per element → ~20 near-blank
pages (one donut/legend line each). "mise en page ko".

- _donut_ax: draw a donut + baked matplotlib legend onto an axes.
- _mpl_donut_grid_png: render the 4 device donuts as ONE 2x2 figure (legends
  baked) → single embedded PNG. _pdf_donut_grid just embeds it.
- _mpl_donut_png: single donut + baked legend (wide) for the glance "Qui te
  trace"; _pdf_donut is now flow-based (no absolute coords / legend cells).
- _ensure_space() page-break guard before the grid, glance and carto images.

Verified on gk2: report back to 4 pages; page 2 = clean 2x2 donut grid + carto +
tables; donut colours match their legends.
2026-06-22 12:51:56 +02:00
CyberMind
6aef15bdf7
Merge pull request #715 from CyberMind-FR/feature/714-kbin-pdf-charts-blank-in-some-viewers-re
PDF: render charts as matplotlib PNGs — fix blank/broken graphs in viewers (closes #714)
2026-06-22 12:34:32 +02:00
ddfe6a7a74 fix(toolbox): render PDF charts as matplotlib PNGs — fix blank/broken graphs in viewers (closes #714)
fpdf2 vector arc/ellipse donuts + carto rendered in poppler but came out blank
in iOS/Chrome PDF viewers (reported: "page 2 KO, dpi", pages blanches, erreurs
de graphs). Render all charts with matplotlib (Agg) to PNG and embed via
pdf.image() — raster displays in every viewer.

- _mpl_donut_png: real donut ring (wedgeprops width) + centre label; _pdf_donut
  embeds it + keeps the fpdf2 text legend.
- _mpl_carto_png: matplotlib hub graph (TOI centre, nodes sized by hits, ISO +
  domain labels, spokes); _carto_graph embeds it.
- bars/tables stay as fpdf2 filled rects + text (render everywhere).
- fix #701 _donut_lines: reset X so section titles aren't clipped at the right.

Verified on gk2 (poppler): page 1 "En un coup d'œil" donut ring + bars; page 2
DPI/MITM/Certs/Pubs rings + carto hub render cleanly as images.
2026-06-22 12:34:25 +02:00
CyberMind
2029611010
Merge pull request #713 from CyberMind-FR/feature/711-kbin-pdf-donut-charts-render-as-solid-pi
PDF: ring donuts + 'En un coup d'œil' section (closes #711, #712)
2026-06-22 12:23:24 +02:00
a2e342cfd2 fix(toolbox): donut charts as true rings + add "En un coup d'œil" to the PDF (closes #711, closes #712)
#711 — _pdf_donut drew filled sectors + a white hole, which read as a solid pie.
Redraw each segment as a THICK STROKED arc (pdf.arc, line_width = band width) on
a faint full-ring underlay → a real concentric ring/annulus.

#712 — _glance_section + _bars render the HTML report's "📊 En un coup d'œil"
card in the PDF: trackers ring + "Vers quels pays" + "Où tu es le plus pisté"
horizontal bars (from report charts). api.py passes charts + graph_stats.

Verified on gk2: page 1 shows the multi-segment trackers ring + the two bar
groups; the 4 device donuts (DPI/MITM/Certs/Pubs) render as rings too.
2026-06-22 12:23:17 +02:00
CyberMind
b5764cb52c
Merge pull request #710 from CyberMind-FR/feature/709-kbin-pdf-integrate-carto-graph-emoji-dat
kbin PDF: carto network map + emoji data tables (closes #709)
2026-06-22 12:17:55 +02:00
e9b20cdd44 feat(toolbox): carto network map + emoji data tables in the PDF (closes #709)
- _carto_graph: radial "carto" (TOI hub → top-8 trackers, nodes sized by hits,
  country flag + domain labels, spokes) mirroring /social/me, drawn with fpdf2.
- _emoji_table: generic emoji table; render Traceurs (flag/domain/hits/sites),
  Pays (flag/iso/trackers/hits) and DPI top-destinations (cat/service/part).
- api.py report_me: pass carto_nodes + carto_country (social graph) to the PDF.

Verified on gk2 (Linux UA): 3-page PDF, page 2 shows the carto hub graph + the
three emoji tables with live data.
2026-06-22 12:17:48 +02:00
CyberMind
28b1c3e91e
Merge pull request #708 from CyberMind-FR/feature/707-kbin-report-cyberpunk-netrunner-characte
kbin report: Cyberpunk-Netrunner character sheet (HTML + PDF) (closes #707)
2026-06-22 12:09:45 +02:00
f4ac537c5a feat(toolbox): Cyberpunk-Netrunner character-sheet report (HTML + PDF) (closes #707)
Reskins the kbin report as an RPG netrunner fiche, driven entirely by LIVE data:
- Persona: tag VILLAGE3B·#xxxx, device class + emoji from the request User-Agent
  (live device, not the stale onboarding label), level = R3 when the client is a
  wg-toolbox peer (_is_wg_r3_peer) regardless of the stored pref, alignement from
  exposure.
- Bars: ICE/intégrité (100-exposure) + Exposition; XP = Ko exchanged (7d, DPI).
- 4 CARACTÉRISTIQUES (Défense/Discrétion/Riposte/Intel) as pip bars, derived from
  protections active + level, trackers, ads blocked, DPI category/proto diversity.
- Inventaire (Tor/Cert-MITM/WireGuard/Ad-block on·off), Bestiaire (top trackers),
  Quêtes (DPI exfil alerts).
- HTML: neon cyberpunk sheet above the Pistage/DPI/Overall dossiers.
- PDF: _persona_block (FICHE NETRUNNER) replaces the zeroed events dashboard.

Verified live on gk2: Linux/Firefox UA → 🐧 Ordinateur Linux · R3; PDF renders
the sheet + attribute tiles + bestiary, donut grid multi-segment.
2026-06-22 12:09:38 +02:00
CyberMind
9d1c227faf
Merge pull request #706 from CyberMind-FR/feature/705-dpi-report-shows-zeros-exfil-is-a-60s-sn
dpi: cumulative 7d per-device rollup — report no longer shows zeros (closes #705)
2026-06-22 11:55:40 +02:00
874e26201f feat(dpi): cumulative 7d per-device rollup — report no longer shows zeros (closes #705)
The kbin report DPI-Exfil tab + PDF donuts showed all zeros whenever the device
was idle: /exfil = state.json is the last 60s capture window only. A per-client
report must reflect what the device did over a meaningful period.

- collector: updateCumulative() merges each window into a persistent 7d store
  /var/lib/secubox/dpi/cumulative.json (same consumer schema as state.json:
  devices[] + top_apps/top_protocols/alerts), summing flows/bytes/by_category/
  services/protocols per device, pruning devices+alerts older than 7d, capping
  services at 80/device.
- api.py: _dpi_stats reads cumulative.json first, falls back to state.json. The
  live dashboard keeps reading state.json (now-view) via /exfil.

Verified on gk2: idle device e6b6ca13… now reports 57 flux / 0.1 Mo over 7d
(was 0); cumulative retains 2 devices across windows.
2026-06-22 11:55:34 +02:00
CyberMind
268cee46fb
Merge pull request #704 from CyberMind-FR/feature/703-kbin-pdf-render-visual-donut-charts-mitm
kbin PDF: visual donut charts (mitm/certs/ads/dpi) for device stats (closes #703)
2026-06-22 11:43:18 +02:00
a66217282f feat(toolbox): visual donut charts in the PDF report (mitm/certs/ads/dpi) (closes #703)
The PDF now renders a "STATS DE TON APPAREIL (graphiques)" 2x2 grid of real
donut charts (fpdf2 solid_arc sectors + white centre hole + legend), not just
text — for this device's:
- 🛰️ DPI — service categories
- 🔍 MITM — nDPI protocol mix
- 🔒 Certs — TLS-trust split (Inspecté TLS / Opaque QUIC / Autre, from the proto mix)
- 🚫 Pubs bloquées — top blocked ad hosts (store.ad_client_stats), total in the hole

All from LIVE sources (DPI collector + ad-block SQLite); the frozen events table
is never read. api.py: _build_pdf_donuts(); reports.py: _pdf_donut + _pdf_donut_grid.

Visually verified via pdftoppm on gk2: 4 donuts render with holes, multi-segment
colours, legends, and live ad counts (~21.8k blocked).
2026-06-22 11:43:11 +02:00
CyberMind
f5da2f6aa8
Merge pull request #702 from CyberMind-FR/feature/701-kbin-pdf-report-include-dpi-exfil-stats
kbin PDF report: include DPI-Exfil stats (closes #701)
2026-06-22 11:33:17 +02:00
d4c268e6e4 feat(toolbox): include DPI-Exfil stats in the PDF report (closes #701)
PDF parity with the HTML report tabs (#699). /report/me now embeds a
"DPI / EXFILTRATION (TUNNEL R3)" section in the fpdf2 report:
- this device: flows / Mo up / Mo down / alert KPIs + category, protocol,
  exfil-alert and top-destination breakdowns (with %).
- overall: board-wide device/flow/alert counts + global category breakdown.

api.py passes dpi_exfil=_dpi_stats(mac_hash) into the report data; reports.py
renders the section (fail-soft: "no DPI data" line when the device wasn't in
the latest capture window).

Live on gk2 (kbin + admin 200): 114 KB PDF renders the section with emoji
category breakdown.
2026-06-22 11:33:10 +02:00
CyberMind
72514ca678
Merge pull request #700 from CyberMind-FR/feature/699-kbin-report-tabbed-donut-stats-pistage-d
kbin report: tabbed donut stats (Pistage / DPI-Exfil / Overall) (closes #699)
2026-06-22 11:25:19 +02:00
a54ad6ab04 feat(toolbox): kbin report — tabbed donut stats with DPI engine (closes #699)
The /report/me/html report now has three tabs:
- Pistage   — existing tracking donuts (trackers/countries/sites), unchanged.
- DPI-Exfil — THIS device's egress from the secubox-dpi collector (matched by
  the same wg-hash identity): donuts for service categories, nDPI protocols,
  exfil-alert kinds, and top destinations by upload + a flows/up/down/alerts
  KPI row.
- Overall   — board-wide DPI donuts (all R3 devices) from the collector rollups.

- api.py: _dpi_stats(mac_hash) reads /var/lib/secubox/dpi/state.json and builds
  conic-gradient-ready donut data (me + overall); passed as dpi_exfil (avoids
  the existing session 'dpi' key). _dpi_donut() = top-N + pct + cumulative
  start/end. Fail-empty before the first capture window.
- report-live.html.j2: tab bar + reusable {donut} macro + DPI/Overall panes;
  JS persists the active tab across the 20s meta-refresh via #hash.

Live on gk2 (kbin + admin both 200): device 4fec81d… renders media/protocol/
destination donuts; tracking tab unchanged.
2026-06-22 11:25:12 +02:00
CyberMind
434d3aba6a
Merge pull request #698 from CyberMind-FR/feature/697-sbxmitm-truncates-responses-8mib-large-g
sbxmitm: stream non-injected responses verbatim — stop truncating >8MiB (closes #697)
2026-06-22 10:54:05 +02:00
9332e1b44b fix(sbxmitm): stream non-injected responses verbatim — stop truncating >8MiB (closes #697)
The response handler read EVERY body with io.ReadAll(LimitReader(8MiB)) and
writeResponse emitted exactly len(body), so any response larger than 8 MiB was
silently truncated — for all content types, not just the 2xx text/html we
inject into. Large Gmail message bodies / attachments / inline images were cut
off, so "certains mails ne s'affichent plus" through the R3 tunnel (same class
as the earlier APK-over-mitm corruption).

- new streamResponse(): writes status+headers then io.Copy(resp.Body) — never
  buffers the whole body; preserves upstream Content-Length, else Connection:
  close delimits. Optional prefix for already-peeked bytes.
- handler: only buffer when injectEligible (2xx text/html); everything else
  streams verbatim. Oversized HTML (>8MiB cap) also streams verbatim rather
  than serving a truncated inject. Full interception preserved (still MITM).

Verified live on gk2: 20 MB download through a worker returns all 20,000,000
bytes (was capped at 8 MiB); banner still injects into small text/html.
2026-06-22 10:53:59 +02:00
CyberMind
1282c41e41
Merge pull request #696 from CyberMind-FR/feature/695-dpi-dashboard-fill-top-apps-protocols-ba
dpi 1.1.2: fill all dashboard lists from the exfil engine (closes #695)
2026-06-22 10:46:08 +02:00
8094a75077 feat(dpi): fill all dashboard lists from the exfil engine (closes #695)
The four list cards (Top Applications, Top Protocols, Bandwidth by Device,
Active Flows) showed "No data" — they queried the inactive netifyd backend.
Now driven by the real R3 DPI engine:

- collector emits global rollups in /exfil: top_apps (by service/host),
  top_protocols (by nDPI proto), per-device up+down bytes, active_flows (top
  flows incl. uncategorised dests).
- dashboard renders all four lists + the repointed stat cards from a single
  /exfil fetch; netifyd loaders dropped from the refresh loop.

secubox-dpi 1.1.2. Live on gk2: admin.gk2/dpi/ lists populated (top_apps 10,
top_protocols 10, active_flows 13).
2026-06-22 10:46:02 +02:00
CyberMind
323363e701
Merge pull request #694 from CyberMind-FR/feature/692-dpi-beaconing-rule-fires-on-sub-second-a
Some checks are pending
License Headers / check (push) Waiting to run
dpi 1.1.1: beaconing period-band + dashboard cards on exfil engine (closes #692, #693)
2026-06-22 10:07:40 +02:00
c91931380f fix(dpi): beaconing period-band + dashboard cards on exfil engine (closes #692, closes #693)
#692 — beaconing scenario was firing on sub-second app/media chatter
("~39 ms" false positives). Now requires a C2-plausible cadence: mean IAT in
[1 s, 1 h], CV <= 0.25, >=6 flows, to an EXTERNAL exfil-relevant/unclassified
dest (never known media/game/social CDNs). Detail reads in seconds.

#693 — the DPI dashboard headline stat cards were legacy netifyd widgets,
empty on R3 boards (netifyd inactive). Repointed to the real exfil engine:
R3 Devices / captured Flows / Categories / Exfil Alerts, all from /exfil.
netifyd list cards degrade gracefully.

secubox-dpi 1.1.1. Verified live on gk2: 39 ms synthetic beacon dropped, 5 s
beacon fires; admin.gk2/dpi/ stat cards + exfil panel populated.
2026-06-22 10:07:24 +02:00
CyberMind
dde96f212e
Merge pull request #691 from CyberMind-FR/feature/689-sbxmitm-forged-leaf-certs-expire-after-2
sbxmitm: forged leaves valid 365d not 24h — stop daily cert expiry (closes #689)
2026-06-22 09:46:57 +02:00
CyberMind
8fae0dab54
Merge pull request #690 from CyberMind-FR/feature/687-plan-full-flow-dpi-on-r3-ndpid-netifyd-p
DPI: per-device R3 cloud-exfiltration pipeline + dashboard, packaged (closes #687)
2026-06-22 09:46:53 +02:00
997fa0501d chore(dpi): gitignore Go build artifacts (ref #687) 2026-06-22 09:44:42 +02:00
1567f94184 build(dpi): package the R3 exfil pipeline as a proper .deb (ref #687)
Ship Phase 2/3 instead of scp-deploying it:
- Architecture all -> arm64 (now carries a compiled collector).
- debian/rules builds the pure-stdlib Go collector offline for arm64
  (GOTOOLCHAIN=local, GOPROXY=off, CGO off) and installs:
    /usr/sbin/secubox-dpi-collector
    /usr/sbin/secubox-dpi-flowcap
    /usr/lib/systemd/system/secubox-dpi-flowcap.service (dh_installsystemd
    auto-enables + starts it)
- control: Depends libndpi-bin (ndpiReader); Build-Depends golang-go.
- postinst pre-creates /var/lib/secubox/dpi (0755) so the collector (root,
  0644 state.json) and the dpi API (secubox) interoperate.
- changelog 1.1.0-1~bookworm1.

Validated on gk2: dpkg upgrade 1.0.5 -> 1.1.0; both secubox-dpi and
secubox-dpi-flowcap enabled+active from the packaged units; /api/v1/dpi/exfil
serving live; libndpi-bin dependency satisfied.
2026-06-22 09:44:29 +02:00
7b379a03d6 fix(sbxmitm): forged leaves valid 365d, not 24h — stop daily cert expiry (closes #689)
The Go MITM engine forged leaf certs with only 24h validity while the per-host
cert cache never evicts. A worker running >24h kept serving the same now-expired
leaf, so every client (notably iOS Safari) reported "certificat expiré" for any
forged site — most visibly kbin.gk2.secubox.in. A worker restart masked it for
~24h, then it recurred.

Forge leaves with NotBefore=now-48h (clock-skew) and NotAfter=now+365d (367d
total span, safely under Apple's 398-day server-cert max-validity rule). Full
interception is preserved — no splice/passthrough.

Verified live on gk2: forged kbin leaf now nb=2026-06-20 na=2027-06-22, issuer
still "Gondwana ToolBoX R3 CA".
2026-06-22 09:19:04 +02:00
01b35e7b95 feat(dpi): service categorization + exfil dashboard panel (ref #687)
"dpidify" R3 egress beyond cloud-only: classify every flow's SNI into nDPI-style
categories so the operator sees *what* each device does, not just whether it hit
a cloud.

collector:
- classify(sni) → (category, service); longest-suffix match for determinism.
  Categories: cloud, filehost, messaging, ai, media, game, social, adult.
- exfil scenarios (volume / new-dest) now fire on exfilCat() = the data-leak-
  relevant set {cloud, filehost, messaging, ai}; media/game/social/adult are
  shown but never alerted (browsing, not exfiltration).
- state per device now carries services[] (all categorized egress) +
  by_category{} flow counts, alongside the back-compat clouds[] subset.
  alert/agg gain category+service fields (cloud kept for back-compat).

dashboard (www/dpi/index.html):
- new "🛰️ Cloud Exfiltration Watch" card: severity-first alert feed +
  per-device egress grouped by category with colored chips and per-service
  ↑/↓ byte counts; exfil-relevant rows flagged red. Polls GET /exfil.

Verified live on gk2 against a real ndpiReader capture: device classified
adult (Chaturbate) + cloud (Google APIs) across 23 flows; CSV columns confirmed
matching (#flow_id/src_ip/server_name_sni/c_to_s_bytes/iat_flow_*).
2026-06-22 08:53:05 +02:00
76acf259c2 feat(dpi): per-device cloud-exfiltration pipeline on R3 (ref #687)
Phase 2 — turn the R3 tap (wg-toolbox) into a per-device DPI exfil sensor,
modeled on how CrowdSec feeds the WAF: a lean C producer feeds a Go scorer
that attributes flows to devices and fires cloud-exfiltration scenarios.

- collector/main.go: pure-stdlib Go scorer. Reads ndpiReader CSV, attributes
  each flow to a device via sha256(wg_pubkey)[:16] from wg-peers.json, maps
  SNI/dest to known clouds, and fires scenarios:
    * exfil_volume  — >=5MB upstream to a cloud and up>down
    * new_cloud     — first contact with a cloud dest for this device
    * beaconing     — low-jitter periodic flows (IAT CV<=0.25)
    * unclassified_external — uncategorised egress to non-local dests
  Writes /var/lib/secubox/dpi/{state.json,seen.json}. CGO-free, ~2MB static.
- sbin/secubox-dpi-flowcap: fixed-window capture loop (ndpiReader -C CSV)
  → collector. Idle-safe if libndpi-bin is absent.
- systemd/secubox-dpi-flowcap.service: Nice 15, MemoryMax 256M, CPUWeight 20,
  CAP_NET_RAW/NET_ADMIN only. Light on a saturated board (~1% CPU observed).
- api/main.py: GET /api/v1/dpi/exfil serves state.json (fail-empty).

ndpiReader is the PoC producer; an nDPId JSON-socket daemon is the production
upgrade (Phase 3). Live on gk2: flowcap active, state.json refreshing.
2026-06-22 08:27:43 +02:00
9eb2d68b92 fix(toolbox): content-aware binary streaming — restore banner on heavy sites
Some checks failed
License Headers / check (push) Has been cancelled
The #685 stream_large_bodies=1m streamed large HTML too (streamed bodies can't be
banner-injected) → "plus de banner sur leparisien.fr". Replaced by the
stream_binaries addon: streams only large NON-HTML (apk/xpi/video/octet-stream/
big downloads) verbatim so the R3 forging path doesn't corrupt them, while HTML
is always buffered so inject_banner + ad_ghost work. toolbox 2.7.16.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:24:17 +02:00
cd3bbcadf3 fix(toolbox): report reads LIVE social graph, not frozen events (ref #686)
/report/me/html showed all-zeros while /social + the webext showed data — it
read the events table (frozen since the #662 cutover). Now it pulls
social.fetch_graph (7d) and drives the gauge (exposure score), KPIs (traceurs/
sites/pays/anti-bot/opérateur/liens) and graphs (trackers donut, countries bars,
top-pisté-sites bars) off the live graph. Verified live: 9433ceb9 → 113 traceurs
/183 sites/13 pays (was 0). toolbox 2.7.15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:20:49 +02:00
00184bdbec docs: session checkpoint 2026-06-20 — Tor shipped, client releases, ad-block/mitm hardening, #686 open
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 08:08:50 +02:00
c7cc7bbe33 fix(toolbox): mitm-wg stream large bodies — stop corrupting APK/cert via R3 (ref #685)
Large binary downloads (14MB APK, CA cert) were corrupted/truncated ONLY through
the R3 WG tunnel (user: "disabling the wg and the apk is okay"). The HTTP/2
forging path buffered+reframed them. Set stream_large_bodies=1m so big responses
pass through verbatim. No addon touches non-HTML bodies (all text/html-gated),
so streaming is byte-transparent. toolbox 2.7.14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 07:28:21 +02:00
b936b2dbf7 fix(toolbox): harden ad-learner — never 204 functional infra (ref #685)
The learner promoted ad candidates seen on a single site (AD_MIN_SITES=1) with
no functional-infra guard, so it hard-blocked www.google.com → broke reCAPTCHA/
consent on news sites (euronews). Now:
- NEVER_LEARN guard: Google/CDN/fonts/captcha/auth/payment registrables matched
  on host + registrable (env-extendable via SECUBOX_NEVER_LEARN).
- AD_MIN_SITES default 1 → 2 (a one-site host no longer auto-blocks globally).
- Existing never-learn / allowlisted entries PRUNED from learned-trackers.txt
  each run (cleans previously mis-learned hosts).

Verified live on gk2: www.google.com/.fr pruned; real trackers (GA) stay blocked.
toolbox 2.7.13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 07:18:53 +02:00
1019 changed files with 253482 additions and 7943 deletions

View File

@ -3,6 +3,237 @@
--- ---
## 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) ## 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 / - **Switch + tunnel** for routing kbin surfing through Tor, shipped **default-OFF /
@ -6527,3 +6758,19 @@ 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

@ -1,5 +1,164 @@
# TODO — SecuBox-DEB Backlog # TODO — SecuBox-DEB Backlog
*Mis à jour : 2026-06-19* *Mis à jour : 2026-06-27*
---
## ⚪ 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)
--- ---

View File

@ -1,5 +1,118 @@
# WIP — Work In Progress # WIP — Work In Progress
*Mis à jour : 2026-06-19* *Mis à jour : 2026-06-27*
---
## ✅ 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.
--- ---

View File

@ -48,6 +48,7 @@ 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

@ -63,6 +63,11 @@ 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" \
@ -152,7 +157,12 @@ 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 dh-python python3-all python3-setuptools golang-go
# 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
@ -213,7 +223,18 @@ 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

@ -19,7 +19,7 @@ network:
bridges: bridges:
br-lan: br-lan:
interfaces: [lan0, lan1, lan2, lan3] interfaces: [lan0, lan1, lan2, lan3]
addresses: [192.168.1.1/24] addresses: [192.168.10.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.1.1/24] addresses: [192.168.10.1/24]
dhcp4: false dhcp4: false
parameters: parameters:
stp: false stp: false

View File

@ -6,16 +6,21 @@ network:
renderer: networkd renderer: networkd
ethernets: ethernets:
# WAN — connecté à l'opérateur # WAN candidate (SFP+, eth0) — connecté à l'opérateur via fibre/module SFP.
eth0: eth0:
dhcp4: true dhcp4: true
dhcp6: false dhcp6: false
optional: true optional: true
# LAN — ports GbE (DSA ou directs selon la config switch) # LAN — port GbE switch (DSA 88E6341)
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
@ -31,8 +36,8 @@ network:
bridges: bridges:
# Bridge LAN # Bridge LAN
br-lan: br-lan:
interfaces: [eth1, eth2, eth3, eth4] interfaces: [eth1, eth3, eth4]
addresses: [192.168.1.1/24] addresses: [192.168.10.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.100.1/24] addresses: [192.168.10.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.100.1/24] addresses: [192.168.10.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.1.1/24 - 192.168.10.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.1.1/24] addresses: [192.168.10.1/24]
dhcp4: false dhcp4: false
parameters: parameters:
stp: false stp: false

View File

@ -55,4 +55,13 @@ 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;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 49 KiB

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