Compare commits

...

586 Commits

Author SHA1 Message Date
d05dcf615e fix(metrics): real Top-Vhosts (nginx per-vhost logs) + WAF block rate (waf-threats.log)
Some checks are pending
License Headers / check (push) Waiting to run
live_hosts now counts per-vhost requests from /var/log/nginx/<vhost>_access.log
over the window (this topology has one HAProxy frontend, so no per-vhost
signal there) + exposes total_requests. health/summary WAF rate reads the
live sbxwaf threat log over the last hour as a share of traffic (was the
stale /var/log/mitmproxy path + a //10 heuristic); adds waf.blocks_1h.
postinst adds the service user to 'adm' for nginx-log read. Bumps 1.0.5.
2026-06-29 17:34:56 +02:00
877fb9e19a fix(portal): bind Active bans to /crowdsec/decisions count (2.2.2) 2026-06-29 17:25:16 +02:00
5e4c0d2dac fix(portal): reskin to standard SecuBox template (shared sidebar + C3BOX palette)
Replace the bespoke top navbar with the shared menu-driven sidebar, swap the
P31 green theme for design-tokens/crt-light (C3BOX), wrap content in
main.main + crt-engine. Dashboard content, IDs and data-fetch JS unchanged.
Bumps secubox-portal 2.2.0 -> 2.2.1.
2026-06-29 16:49:30 +02:00
de15937ccf fix(appstore): navbar entry -> /usr/share/secubox/menu.d + 'root' section (0.2.2) 2026-06-29 16:43:26 +02:00
e51a310010 fix(appstore): NoNewPrivileges=no for sudo bridge + try-restart on upgrade (0.2.1) 2026-06-29 16:36:20 +02:00
6034dfb0c3 feat(appstore): Phase B/C — navbar integration + quick actions + config editor
UI integrates the shared sidebar (nav#sidebar + sidebar.js + crt-engine) and
adds per-service quick actions (start/stop/restart/enable/disable) + a config
editor drawer (view/edit module TOML, shows deps). API: POST
/module/{name}/action/{verb}, GET/PUT /module/{name}/config. Privileged work
via validated root helper secubox-appstorectl (secubox-* units +
/etc/secubox/<name>.toml only, TOML-validated writes) + narrow sudoers.
postinst rebuilds the hub menu cache so the store shows in the navbar.
Bumps 0.2.0.
2026-06-29 16:34:17 +02:00
8f46bcb93b Merge branch 'feature/appstore-phase-a' — App Store Phase A (read-only catalog) 2026-06-29 13:20:49 +02:00
58f1f1a2c8 fix(appstore): serve catalog API from secubox-routes.d (beats /api catch-all)
The /api/v1/appstore/ route must live in secubox-routes.d/ (the hub's
authoritative API include) or the generic /api/ -> aggregator catch-all
swallows it (404). Split: API route -> secubox-routes.d/appstore-routes.conf
(standalone appstore.sock), UI -> secubox.d/appstore.conf. Bumps 0.1.1.
2026-06-29 13:20:17 +02:00
66301f4307 feat(appstore): SecuBox App Store Phase A — read-only module catalog
New secubox-appstore module: api/main.py serves /catalog (category/tier/
state/q), /module/{name}, /categories, /health — merging a build-time
catalog (generated from every module's debian/secubox.yaml) with live dpkg
+ systemctl state (available/installed/running/tier-locked). Categorized,
tiered, searchable grid UI at /appstore/. Standalone service on
/run/secubox/appstore.sock, nginx route + static (no aggregator dependency).
Also backfills secubox-lyrion's missing manifest.
2026-06-29 13:16:37 +02:00
a9f349a57d docs(appstore): SecuBox App Store + Module Composer architecture sketch + GPT prompt
Catalog over the 128 secubox.yaml manifests; granular control
(module/component/navbar/appearance); profiles (4R atomic apply); P2P-mirrored
apt repo + federated catalog; multi-service mesh agents. Includes a complete
GPT architectural-research-notes prompt (§10).
2026-06-29 13:09:15 +02:00
a1ec2601c8 fix(p2p): render P2P web UI — API-shape handling + live mesh graph
JS read /peers and /threats as arrays but the API returns {peers,count} /
a dict -> nothing rendered. Handle both shapes; loadOverview uses
service_count. Mesh canvas was a hardcoded placeholder; now wired to GET
/mesh. /mesh derives nodes/links from wg_mesh.json (local + wg peers).
Bumps secubox-p2p 1.7.7 -> 1.7.8.
2026-06-29 13:03:03 +02:00
a0f9c7811f fix(p2p): P2P web UI empty — surface the wg-mesh in /peers + /status
The dashboard read the unused legacy peers.json; the live mesh lives in
wg_mesh.json (Phase 1). Add mesh.peer_nodes() and drive /peers + /status
from it; /status now returns total_peers/active_peers (the fields the web
UI actually reads). Per-peer name + mesh_ip give friendly node labels.
Bumps secubox-p2p 1.7.6 -> 1.7.7.
2026-06-29 12:45:09 +02:00
b8fce891de Merge branch 'feature/gondwana-phase1-mesh' — Gondwana Phase 1 mesh substrate
secubox-p2p becomes the single WireGuard mesh owner: subnet collision fixed
(10.100.0.0/24 -> 10.10.0.0/24), port 51822, pure api/mesh.py (collision
guard, p2p.toml loader, master-assigned IP, wg.conf parse/render, key
adoption, DDNS name), root sbx-mesh-up provisioner, secubox-p2p 1.7.6.
Live: gk2 (.1 master, key-preserving state sync, no bounce) + c3box (.2) +
amd64 (.3) — full 3-node mesh verified alive.
2026-06-29 12:19:05 +02:00
7effe5fb1a fix(p2p): delegate provisioning to sbx-mesh-up, mesh_ip on manual approve, top import, SPDX/test polish
- enable_wireguard: remove inline wg-quick + wg.conf render (KeyError on
  roaming peers without endpoint, fails as secubox user); now only sets
  enabled=true and returns a message directing root to run sbx-mesh-up
- _assign_mesh_ip helper DRYs allocation; ml_approve now also allocates
  mesh_ip for manually-approved peers (was silently None before)
- `from . import mesh` moved from mid-file WireGuard section to top import block
- SPDX headers added to tests/conftest.py and tests/test_mesh.py
- `import pytest` moved to top-level in test_mesh.py; redundant sys.path.insert removed
- ddns_name: slug[:63] RFC label cap + empty-string falls back to "node";
  test_ddns_name_empty_falls_back added (17 tests, all green)
2026-06-29 11:59:52 +02:00
b080612396 build(p2p): ship p2p.toml.example + sbx-mesh-up, dep wireguard-tools, 1.7.6 2026-06-29 11:46:04 +02:00
96c048860d fix(p2p): drop unused subprocess import + PKG var in sbx-mesh-up, assert listen_port
Cleanups in secubox-p2p (Task 7 follow-up):
- Remove unused 'subprocess' import from sbx-mesh-up heredoc
- Remove unused 'PKG' shell variable (path hardcoded in sys.path.insert)
- Add listen_port assertion in test_adopt_state_imports_existing_key_when_absent
2026-06-29 11:44:25 +02:00
7e75efffd2 feat(p2p): root sbx-mesh-up provisioner (adopt key, guard, render, up) 2026-06-29 11:42:10 +02:00
910b87fd3a fix(p2p): drop colliding 10.100.0.x fallbacks from enable_wireguard
Add address guard (400 if /wireguard/init never called), use
config['address'] directly (no fallback), replace hardcoded peer
AllowedIPs fallback '10.100.0.0/24' with mesh.MESH_NETWORK constant.
Collision guard now covers all code paths.
2026-06-29 11:40:04 +02:00
a3ec30ed96 feat(p2p): adopt mesh.py — 10.10.0.0/24:51822, role-aware addressing, collision guard 2026-06-29 11:35:37 +02:00
8ef46e086b feat(p2p): per-node DDNS identity name helper 2026-06-29 11:31:36 +02:00
a949b2e495 feat(p2p): parse/render wg-mesh.conf (key adoption + provisioning) 2026-06-29 11:29:33 +02:00
6a662a165c feat(p2p): master-assigned mesh IP allocation (.2+, .1=master) 2026-06-29 11:26:34 +02:00
29897b40bc feat(p2p): /etc/secubox/p2p.toml [wireguard] loader + example 2026-06-29 11:23:54 +02:00
d70db5ea7e feat(p2p): mesh module with subnet collision guard 2026-06-29 11:20:55 +02:00
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
5bd107cb46 chore(toolbox): serve new clients from kbin — pin webext v0.1.5 (ref #683)
Bump the pinned webext release tag v0.1.4 → v0.1.5 in the /wg/toolbox.xpi
fallback (api.py) and secubox-toolbox-fetch-xpi. APK serve path already pulls
/releases/latest (android-v0.4.0). toolbox 2.7.12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 06:53:07 +02:00
CyberMind
69d4f0bd5c
Merge pull request #684 from CyberMind-FR/feature/683-plan-kbin-tor-endpoint-quick-switch-anon
Some checks are pending
License Headers / check (push) Waiting to run
feat(#683): kbin Tor egress quick-switch + clients + landing/report redesign
2026-06-19 21:30:13 +02:00
47eae4a774 feat(toolbox): restyle landing to match the new report (ref #683)
System font, rounded --panel/--line cards, cleaner accents, softer rounded SVG
bars + helper lines; R3 panel + arch note mention the 🧅 Tor egress option.
Dynamic bits unchanged: live KPIs + auto-refresh JS, per-OS install panels,
cert-probe, ?mh links. Verified live on kbin (2.7.11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:18:59 +02:00
db5d5dbcf1 feat(toolbox): regenerate /report — verdict-first, graphs, collapsible (ref #683)
Simple, easy-to-process report:
- Verdict hero: conic-gradient score gauge + plain-language verdict + helper.
- 6 KPIs (connexions/hôtes/trackers/pays/apps/cert-pin).
- 3 graphs computed server-side (_build_report_charts): trackers donut
  (conic-gradient), countries bars, apps bars — with one-line helpers.
- ALL deep technical cards (threat-intel/DGA/beaconing, hosts, apps, cookies,
  avatar, transparency+per-host grades, identity, reco) collapsed into <details>.
- Mobile-first, system-font, rounded cards.
- /report/me/html resolves identity via shared _client_mac_hash (?mh → R3 WG
  peer → captive ARP) so R3 clients reach it without ?mh.

Verified live on kbin: 200, 11.7KB, gauge + 3 graphs + details render for an
R3 peer via X-R3-Peer and via ?mh. toolbox 2.7.10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:07:41 +02:00
41dbdadaa2 fix(toolbox): landing report/carto links carry ?mh= → no 'identity unresolved' (ref #683)
Clicking "Ma carto" / "Mon rapport" / "Qui me piste ?" hit /social/me +
/report/me with no ?mh=, so identity was re-resolved at click-time and could
400 "client identity unresolved" (off-tunnel/captive, or when X-R3-Peer wasn't
present on that request). The landing already knows the caller — now it resolves
mac_hash (new _client_mac_hash: ?mh → R3 WG peer → captive ARP) and bakes ?mh=
into the links so they always open the right client's view.

Verified live: R3 peer 10.99.1.2 → links carry ?mh=1b0ec958…; captive caller →
?mh from ARP. toolbox 2.7.8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:32:35 +02:00
e1b2e6ccbb fix(toolbox): injected banner trackers/cookies stuck at 0 — count live (ref #683)
The bar counted trackers (Resource Timing) + cookies (document.cookie) ONCE at
render time, which fires early — before resources/cookies have loaded — so it
showed 0, and the 2s poll's ensure() early-returned once the banner existed, so
it never refreshed. Spans now carry ids (sbx-trk/sbx-ck) and updateCounts()
re-counts on the poll → values climb to real within ~2s. toolbox 2.7.7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:21:04 +02:00
79c6166181 fix(toolbox): 🧅 Tor indicator on the REAL injected banner (bundle) (ref #683)
The live page banner is the client-side stream-inject bundle (bundle.py:
"SecuBox · LEVEL · 🛰️ trackers · 🍪 cookies · report ▸ · ✕"), not the
server-side inject_banner chip I'd added earlier. Added tor_mode to the
decision bundle + a "🧅 Tor" span in the shared banner render() (_BANNER_CORE,
used by both the loader and the #662 inline/service-worker path).

Verified live on kbin: /__toolbox/bundle → tor_mode:true; loader.js + inline
both carry the 🧅 span. toolbox 2.7.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:13:13 +02:00
55955867af feat(clients): persistent WG identity (APK) + Tor status in webext/APK (ref #683)
APK (0.4.0):
- FIX lost-referrer: persist the WG profile in app-internal filesDir and REUSE
  it. /wg/profile/new mints a fresh keypair each call and onboarding runs every
  boot, so the device kept re-keying → new sha256(pubkey) identity → stats reset
  each reboot. Now one stable identity across reboot/reconnect/restart.
  (Reinstall still wipes filesDir; allowBackup stays off for CSPN.)
- Silent root onboarding (CA system-store + native WG) already runs on boot
  (#538/#558); it now provisions the STABLE profile.
- Surfaces 🧅 kbin Tor-egress status after onboarding.

webext (0.1.5):
- popup shows a 🧅 Tor indicator (exit anonymised) next to the R3 dot,
  via the new public /wg/tor-status endpoint.

toolbox (2.7.5):
- public, kbin-safe GET /wg/tor-status {tor_mode,running,bootstrap,exit_ip}
  (mirrors /wg/r3-check; /admin/tor/* stays admin-gated). Verified live on kbin.

13 toolbox tests green. .xpi 0.1.5 built; .apk builds via build-android-apk.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:03:15 +02:00
55c7d925a6 fix(toolbox): Tor mode must not torify the box's own services (ref #683)
kbin/admin resolve to the WAN IP (reached via hairpin) and the LAN was not
exempt, so with Tor armed the portal's self-traffic round-tripped through Tor
(~18x slower, measured 0.9s vs 0.05s) → kbin landing + dashboard graphs/metrics
loaded empty/slow for clients behind the MITM, and the banner page degraded.

nft tunnel now carries a reconciler-populated `tor_exempt` set: loopback +
board-local connected subnets (LAN/WG/LXC, from `ip route scope link`) + the
board's own public IP (detected direct). Self-traffic stays DIRECT; real
internet still exits via Tor; DNS stays Tor-routed (no leak); the Tor automap
range (10.192.0.0/10) stays torified (never a connected route).

Verified live on gk2: worker->kbin direct 0.36s/200, worker->internet exits
45.84.107.76 (Tor, != board 82.67.100.75), kbin 200, stats populated. toolbox 2.7.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:05:17 +02:00
982a27ce38 fix(toolbox): dashboard MITM metrics stuck at 0 — derive from real data (ref #683)
The "Live metrics" MITM trio (connections / hosts / cert-pin blocks) was
permanently 0: it scraped "server connect" from journalctl, but the workers run
at --log-level warning so those INFO lines are never emitted. Now sourced from
the cumulative stats (DPI flow count + top-hosts, same as the landing page) and
the auto-learned mitm-bypass-dynamic host count. Verified live on gk2:
connections 29238, unique_hosts 15 (were 0). toolbox 2.7.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:55:45 +02:00
aef5b00b85 feat(toolbox): 🧅 Tor chip on banner + tunnel survives nft reloads (ref #683)
- banner: inject_banner shows a 🧅 Tor chip first when kbin Tor mode is armed,
  so the client sees they are anonymised (filters-driven, 5s-cached).
- fix: the runtime toolbox_tor table was flushed by any nftables reload (other
  postinsts trigger one) → flag-on/tunnel-off leak window. Reconciler now
  persists it as /etc/nftables.d/zz-secubox-toolbox-tor.nft so reloads re-apply
  it; added a 2-min re-arm timer (self-heals bare `nft flush ruleset`) and a
  final postinst reconcile after the nft reloads.
- fix: postinst also restarts secubox-toolbox-mitm-wg.service so mitm addon
  updates take effect without a manual restart.
- toolbox 2.7.2. Verified live on gk2: table survives `systemctl reload
  nftables`, egress stays Tor, kbin 200, no leak window post-upgrade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:03:41 +02:00
03346907e4 fix(toolbox): make the Tor switch actually persist + activate (ref #683)
The switch returned OK but never armed Tor — three stacked perm/sandbox
blockers, found by live debugging on gk2:

1. filters.json was root:root and /etc/secubox/toolbox is 0750 → no service
   user could create the atomic-rename tmp. set_filters now falls back to an
   in-place write (file-write only) — which also reliably fires the .path
   watcher (in-place modify, not a rename).
2. The host portal (the real handler; admin.gk2 proxies to it) had
   ReadOnlyPaths=/etc/secubox → write hit EROFS. Add
   ReadWritePaths=/etc/secubox/toolbox (CA key stays 0600 root, unreadable).
3. filters.json owner fixed to secubox-toolbox so the portal owner-writes;
   postinst re-applies every install to repair pre-existing drift.

Verified live on gk2 (tor_mode armed): worker uid egress = Tor exit
(185.220.101.24) vs real WAN (82.67.100.75); fail-closed confirmed (tor down
→ egress blocked, rc=28, no real-IP leak); kbin stays 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:49:36 +02:00
a870eb380e feat(toolbox): kbin Tor egress quick-switch — switch + tunnel, DARK (ref #683)
Routes the R3 mitm-wg worker uid's upstream egress + DNS through Tor
(TransPort 9040 / DNSPort 5353) via an nft owner-match table, so MITM
inspection (ad-block/poison/banner/safe-browsing) is fully preserved —
only the exit IP + network identity change. Ships default-OFF, fail-closed.

- filters: tor_mode / tor_preset flags (validated)
- api: GET/POST /admin/tor/{state,on,off,newnym,check-leaks} (kbin-gated)
- tor_ctl.py: reuses secubox-tor control-port logic (status/NEWNYM), no JWT
- tunnel: conf/nft-toolbox-tor.nft (fail-closed kill-switch + v6 drop) +
  torrc drop-in + root path-triggered reconciler (portal stays
  NoNewPrivileges=true; nft loaded before tor = no clearnet window)
- WebUI: 🧅 Tor tab (badge, toggle, NEWNYM, leak probe)
- packaging: Depends jq; Recommends tor, python3-socksio; postinst adds
  secubox-toolbox to debian-tor group; prerm disarms on real removal
- 166 tests green (10 new); toolbox 2.7.1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:19:52 +02:00
4c6777dc68 chore(toolbox): 2.7.0 middle release — kbin milestone + Tor chapter (ref #683)
Some checks are pending
License Headers / check (push) Waiting to run
kbin (public ToolBoX portal) framed as the first tool of the CyberMind
Swiss-army cyber kit: transparent perf, full-encrypted MITM inspection,
ad poison/smog injection, adware-ban banner, safe browsing.

- secubox-toolbox 2.6.59 -> 2.7.0 (caps 2.6.x, opens kbin chapter)
- docs: wiki Kbin-Toolbox.md, FAQ-KBIN-TOR.md, README blurb
- plan #683: kbin Tor endpoint (outbound egress quick-switch) — design spec
- WIP/TODO/HISTORY updated

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:48:32 +02:00
CyberMind
1a315317e7
Merge pull request #682 from CyberMind-FR/feat/662-client-geoflag
feat(#662): per-client country flag from real external WG-endpoint IP
2026-06-19 11:24:01 +02:00
CyberMind
04598482fb
Merge pull request #681 from CyberMind-FR/fix/662-altsvc-strip
fix(#662): strip Alt-Svc — stop HTTP/3 so traffic stays on MITM-able TCP
2026-06-19 11:22:23 +02:00
be0497e6de fix(toolbox-ng): strip Alt-Svc to stop HTTP/3 advertisement → keep traffic on MITM-able TCP (ref #662) 2026-06-19 11:21:21 +02:00
7db7a73d65 fix(toolbox): QUIC udp443 reject (not drop) — drop made browsers retry QUIC 199x instead of TCP fallback; reject forces immediate fallback → MITM sees the traffic (ref #662) 2026-06-19 11:18:06 +02:00
3ade5619d0 feat(toolbox): per-client country flag from REAL external WG endpoint IP (ref #662)
/admin/clients/rich geo-resolved the stored client IP, which for WG clients is
the internal 10.99.1.x (GeoIPs to nothing) → empty flags. The true origin is the
peer's pre-tunnel WG endpoint (from wg show wg-toolbox dump).

- wg.wg_endpoints(): parse `wg show wg-toolbox dump`, map sha256(pubkey)[:16]
  → external endpoint IP. Skips (none)/RFC1918/loopback/link-local. Best-effort
  (empty on missing wg/error), cached ~30s — no shell-out per row.
- admin_clients_rich: geo-enrich from the external endpoint when present, else
  fall back to the stored ip (non-WG/captive clients still work). Within ENRICH_LIMIT.
- PRIVACY: external IP used transiently for the GeoIP lookup only — never stored
  or returned. Country-granularity only (flag/ISO + existing asn_org).
2026-06-19 11:16:03 +02:00
a48f43607b fix(toolbox): drop QUIC (UDP443) BEFORE the outbound accept — was after → never fired → HTTP/3 bypassed the whole MITM (no inject/adblock/metrics/social) (ref #662) 2026-06-19 11:10:29 +02:00
CyberMind
27ba48c1a1
Merge pull request #680 from CyberMind-FR/feat/662-inline-banner
feat(#662): inline the banner (SW-immune) — defeat site service workers
2026-06-19 11:02:52 +02:00
c04a9d0c1c chore: changelog 0.1.13 — inline SW-immune banner (ref #662) 2026-06-19 11:01:47 +02:00
3009ef93d9 fix(toolbox): inline transparency banner — survive sites with a service worker (ref #662)
Sites with a SERVICE WORKER (leparisien, cnn…) intercept every same-origin
request, so the legacy <script src="/__toolbox/loader.js"> + its
fetch("/__toolbox/bundle") were hijacked by the page SW (404 / app-shell)
before reaching the MITM engine → banner never appeared. Fix: INLINE the
banner — the engine fetches the complete script body server-side at inject time
and bakes a self-contained <script>…</script> with mh/wg/csp + the bundle as JS
literals. No same-origin fetch for the SW to touch.

Avoids the #653 failure: the inline script reads NO document.currentScript
(null in async) and does NO fetch() — everything is baked as literals.

Python portal:
- new GET /__toolbox/inline?mh=&wg=&csp= → complete inline banner script body.
- refactor bundle.py: extract shared render/SPA/dismiss/countTrackers/🔓 logic
  into _BANNER_CORE; inline_script(mh,wg,csp) bakes the bundle (get_bundle) as a
  JSON literal + mh/wg/csp string literals. Legacy LOADER_JS (src-loader) kept
  working off the same core. </script> breakout hardened (</ → <\/).

Go engine:
- fetchInlineBanner(): GET portal /__toolbox/inline via the short-timeout portal
  client; fail-open (ok=false → skip inject, page intact).
- injectInlineBanner(): idempotent (same bannerGuard), same placement as
  injectLoader, emits an inline <script> (not <script src>).
- live inject path uses the inline banner; injectLoader + /__toolbox/loader.js
  route kept. Cosmetic <style> (already inline, SW-immune) unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:00:27 +02:00
CyberMind
78ad554ece
Merge pull request #679 from CyberMind-FR/feat/662-adlearn
feat(#662): restore + strengthen ad/tracker auto-learn (live-reload, candidate emit, cross-site promotion)
2026-06-19 10:40:07 +02:00
895356dc00 chore: changelog 0.1.12 — ad auto-learn loop + live-reload (ref #662) 2026-06-19 10:37:49 +02:00
4063ae1a95 feat(toolbox): restore + strengthen ad/tracker auto-learn loop (ref #662)
The #662 Go cutover blocked from STATIC lists but never (1) emitted learning
candidates nor (2) live-reloaded the lists, so new adwares slipped through
forever and even autolearn promotions needed a worker restart. Restore the
full loop: feeders/outsiders -> lock to blocklist -> silence (204) -> smog
(poison) -> statistify, fed by BOTH the ad-path heuristic AND cross-site
cookie reuse (the social graph).

Go (packages/secubox-toolbox-ng):
- policy.go: mtime-based live-reload (Part 1, linchpin). Policy now holds the
  backing file paths + per-file last-mtime; maybeReload() (throttled ~15s)
  re-stats each file and atomically swaps the changed map under an RWMutex.
  Decide/shouldPoison take the read lock; allowedSafe() is the lock-taking
  entry for the candidate feed. Covers learned-trackers + ad-allowlist +
  splice seed/learned + pure-trackers. Promotions/edits now take effect with
  NO worker restart.
- adstats.go: ad-candidate learning feed (Part 2). Ports ad_ghost._AD_PATH
  (RE2) + a (host,site)->hits aggregator (cap 20k), drained into the existing
  ad-event payload's new "candidates" list by the same 10s flusher.
- main.go: maybeRecordAdCandidate() on the allow/mitm branch — 3rd-party
  (registrable(host) != registrable(site)) AND _AD_PATH match, gated behind
  the analysis relay flag, O(1) fire-and-forget.

Python (packages/secubox-toolbox):
- api.py: /__toolbox/ad-event now ingests "candidates" ->
  store.record_ad_candidates(); capped, try/except, never 500s.
- secubox-toolbox-autolearn: new _social_feed() promotes any cross-site
  cookie-reuse tracker (>= SECUBOX_SOCIAL_MIN_SITES distinct src_site in a
  recent window) from social_edges into learned-trackers.txt, reusing the
  _ad_feed allowlist/self guard and merge/de-dup.

Smog: confirmed isTracker() already consults the live-reloaded learned set
(blockedByAd), so a promoted cross-site tracker is poisoned automatically once
the policy reloads it — no new poison code.

TDD: reload_test.go (incl. -race concurrency), adcand_test.go,
test_ad_event_candidates.py, test_autolearn_socialfeed.py. Go build (offline
arm64 + darwin), vet, go test -race all green.
2026-06-19 10:36:02 +02:00
CyberMind
77da033371
Merge pull request #678 from CyberMind-FR/feat/662-social-relay
feat(#662): restore /social cross-site tracker graph (faithful social_graph port + block-path correlation)
2026-06-19 10:13:36 +02:00
3850da5479 fix(toolbox-ng): correlate social edges on the block path (blocked trackers carry the cross-site cookie) (ref #662) 2026-06-19 09:58:07 +02:00
040e460876 chore: changelog 0.1.10 — social relay (ref #662) 2026-06-19 09:53:10 +02:00
55f9e4c803 feat(toolbox): restore /social cross-site tracker graph via Go engine + portal ingest (ref #662)
The #662 Phase-7 cutover decommissioned the in-process Python social_graph
addon that fed social.record_edge(), freezing the kbin /social d3 graph
(social_edges -> social_nodes/social_links in toolbox.db).

Go engine (packages/secubox-toolbox-ng/cmd/sbxmitm/social.go):
- cookieIDHash: byte-exact port of social.cookie_id_hash (lower-case
  domain+name, raw value, NUL separators), proven by a shared Python-generated
  fixture (social-cookie-id-fixtures.json) asserted by both social_test.go and
  tests/test_social_parity.py (anti-rig, same discipline as the jar harness).
- isDenyListed + _DEFAULT_DENY_COOKIES set; registrableSocial (the addon's
  _registrable_domain eTLD+1 flavour, distinct from policy.registrable);
  Set-Cookie + request-Cookie 3rd-party edge extraction; CMP consent_state
  (none_seen/pre_consent/post_consent) via a per-(peer,site) in-memory log.
- Edges (hash-only, NEVER raw values) buffered + flushed every 10s to the
  portal /__toolbox/social-event; WG-peer flows only; gated by --social-relay
  (default true); fire-and-forget, never blocks the flow.

Python portal (secubox_toolbox/api.py):
- POST /__toolbox/social-event ingest (sibling of /__toolbox/ad-event, same
  unauthenticated R3-perimeter trust + 2MB body guard): per-row record_edge
  with try/except, cap 5000, always 204; debounced safety fold_recent
  (<= once/60s) so new edges surface promptly between the existing app.py
  social_fold_loop ticks.

Go: build offline arm64+darwin, go vet, go test -race all green.
2026-06-19 09:51:26 +02:00
257fc95182 fix(toolbox): loader.js no-store so SPA/loader updates propagate (was max-age=3600 → stale loaders pinned 1h) (ref #662) 2026-06-19 09:38:24 +02:00
CyberMind
591106ec65
Merge pull request #677 from CyberMind-FR/feat/662-cumulative-live
feat(#662): cumulative-stats reads live module mitm_events (un-freeze kbin page)
2026-06-19 09:32:00 +02:00
CyberMind
15a668829b
Merge pull request #676 from CyberMind-FR/feat/662-analysis-relay
feat(#662): relay per-flow telemetry to dpi/cookies/ja4 analysis sidecars
2026-06-19 09:31:52 +02:00
73b8ad36b1 fix(toolbox): cumulative-stats reads LIVE module sockets, not frozen toolbox.db (ref #662)
The kbin 'Qui te piste?' page (/cumulative-stats.json) read event counts +
top-hosts from toolbox.db's events table, which froze at the #662 Phase-7
cutover. Pull live counts/hosts from the analysis modules over their unix
sockets (dpi/cookies/threat-analyst), with graceful fallback to the legacy
toolbox.db query if every module call fails. sessions/risk/level
distributions read the clients table and are unchanged.
2026-06-19 09:29:54 +02:00
d0db3e87fd chore: changelog 0.1.9 — analysis relay (ref #662) 2026-06-19 09:23:59 +02:00
05c659b4ca feat(toolbox-ng): relay per-flow dpi/cookies/ja4 telemetry to analysis sidecars (ref #662)
Restores the dpi/cookies/ja4 events feeding the kbin "Qui te piste?"
cumulative-stats page, frozen since the Phase-7 cutover decommissioned
the Python mitmproxy relay addons. The Go engine now re-emits EXACTLY
what those addons did, via the existing fire-and-forget emit() helper.

- relay.go: pure payload builders (dpiEvent/cookiesEvent/ja4Event) +
  gated emit wrappers. NAMES ONLY for cookies (never values, CSPN);
  caps ≤30 set / ≤50 sent names, name[:32], url[:300]; user_agent null
  when absent; ja4 extensions always null (stdlib doesn't expose them);
  alpn/ciphers always JSON arrays.
- main.go: --analysis-relay flag (default true) → Proxy.analysisRelay;
  dpi emit in mitmPipeline allow/mitm branch (before anonymize, original
  UA); cookies emit after resp; serverTLSConfigCapture hook relaying ja4
  with the client conn peer IP; peerIP helper.
- transparent.go: ja4 capture wired with the real transparent peer IP.

Fire-and-forget: a dead/slow sidecar socket never blocks or delays the
proxy flow (emit detaches with its own 2s timeout). Block/splice paths
never relay dpi/cookies; ja4 fires per handshake (blocked/allowed alike,
matching the Python tls_clienthello addon).

TDD: relay_test.go covers payload shapes, names-only parsing, caps,
url truncation, the gate at call sites, and live unix-socket delivery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:23:14 +02:00
ded89934d0 feat(toolbox): SPA-aware banner loader — re-assert on history nav + 2s poll (#655-equivalent, ref #662)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-19 09:10:23 +02:00
CyberMind
9a843cec72
Merge pull request #675 from CyberMind-FR/feat/662-csp-demo
feat(#662): consented CSP-bypass demo + 🔓 proof emoji
2026-06-19 09:07:48 +02:00
8988a1078a refine(toolbox-ng): CSP 🔓 only on directives that actually block the loader (ref #662) 2026-06-19 09:06:36 +02:00
bfc28f1081 chore: changelog 0.1.7 — CSP-bypass demo + proof emoji (ref #662) 2026-06-19 09:01:30 +02:00
05eedca6e8 feat(toolbox): CONSENTED-DEMONSTRATION CSP relax + 🔓 banner proof (ref #662)
The R3 toolbox ("VILLAGE3B — Qui te piste?") is a consented MITM on the
operator's own traffic that SHOWS what a man-in-the-middle can do. A strict
Content-Security-Policy would stop the injected transparency-banner loader
(<script src="/__toolbox/loader.js">) from executing — so on the R3/wg inject
path, gated by the new --csp-bypass-demo flag (DEFAULT TRUE), the engine
deliberately relaxes the page's CSP just enough to let that one same-origin
loader run, then stamps data-csp="1" on the tag and the portal banner renders
a 🔓 as the VISIBLE proof the page's CSP was bypassed to inject. Intentional,
demonstrative, toggleable.

Go (packages/secubox-toolbox-ng):
- new csp.go: relaxCSPForLoader(http.Header) bool rewrites Content-Security-
  Policy AND Content-Security-Policy-Report-Only (all values). For the script-
  governing directive (script-src / script-src-elem; else default-src) it
  ensures 'self'+'unsafe-inline' and strips 'strict-dynamic' (which would make
  host/'self'/'unsafe-inline' ignored) and 'none'. Other directives untouched.
  Returns true iff a real CSP was present and modified (the proof condition);
  never panics on malformed CSP; no CSP → false, unchanged.
- --csp-bypass-demo bool flag (default true) → Proxy.cspDemo. When false, CSP
  is never touched and the proof flag is never set.
- mitmPipeline: only on the responses we actually inject (2xx text/html, R3/wg
  gate) and only when cspDemo, call relaxCSPForLoader(resp.Header) and thread
  the cspBypassed bool through injectIntoBody/injectHTML/injectLoader → the tag
  gets data-csp="1" only when truly bypassed. Never strip CSP on non-injected
  responses.
- csp_test.go: script-src/strict-dynamic/'none'/default-src/report-only/
  malformed/no-header cases + injectLoader data-csp gating.

Python portal (packages/secubox-toolbox):
- bundle.py LOADER_JS reads data-csp off its own <script>; when "1", renders a
  🔓 (title "CSP contourné par SecuBox (démonstration)") in the banner as the
  visible proof. Absent → no proof emoji, banner unchanged.

Offline vendored build (linux/arm64 + darwin/arm64), go vet, go test -race all
green; existing Go + Python (test_bundle*) tests stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 09:00:01 +02:00
40aa2a6a63 docs: record #662 uTLS Chrome anti-bot fingerprint (PR #674) 2026-06-19 08:10:37 +02:00
CyberMind
5e2321e2f9
Merge pull request #674 from CyberMind-FR/feat/662-utls-chrome
feat(#662): Chrome TLS fingerprint (uTLS) upstream + br/zstd inject — defeat anti-bot without splice
2026-06-19 08:10:00 +02:00
464777884b fix(toolbox-ng): H1 response-header timeout on the uTLS transport + changelog 0.1.6 (ref #662) 2026-06-19 08:07:48 +02:00
cbaa35bcb7 feat(toolbox-ng): uTLS Chrome upstream + br/zstd inject, drop AE downgrade (ref #662)
The R3 MITM engine made its upstream TLS handshake with the Go stdlib, whose
JA3/JA4 fingerprint anti-bot vendors (DataDome) block outright. Replace the
upstream transport with a uTLS RoundTripper presenting a current Chrome
ClientHello (HelloChrome_Auto) — WITHOUT splicing, so the body stays fully
inspectable (ad-block / banner / anti-track all keep working) and certificate
verification stays ON (chain + hostname verified against system roots; a
bad/mismatched cert is rejected).

D1 — uchrome.go: uchromeTransport dials the pinned target (transparent →
captured original-dst; CONNECT → request host:port), wraps it in
utls.UClient(..., HelloChrome_Auto) with ServerName=SNI, manually verifies the
chain + hostname after handshake (InsecureSkipVerify set only to take uTLS's
auto-verify out of the path; we run the identical check ourselves and fail on
error), offers Chrome ALPN (h2, http/1.1) and speaks the negotiated protocol
(h2 via x/net/http2 over the established uTLS conn, else HTTP/1.1).
Single-use conns, body-close tears the conn/transport down (no fd leak).
Replaces the stdlib default transport AND the removed transparentTransport on
both the CONNECT PoC and transparent live paths.

D2 — main.go: remove the req.Header.Set("Accept-Encoding","gzip") override;
forward the client's real AE verbatim (overriding it is itself a bot tell).

D3 — gzip.go: injectIntoBody now decodes br (andybalholm/brotli) + zstd
(klauspost/compress) in addition to gzip/identity, runs the existing inject,
and RE-ENCODES in the SAME codec (encoding unchanged). The 32MiB
decompression-bomb cap applies uniformly; fail-open (serve original bytes) on
any decode/encode error; unknown encodings pass through.

First external deps for this engine — all pure-Go, cgo-free (CGO_ENABLED=0
holds): utls v1.6.7 (Go 1.22-compatible), brotli v1.0.6, klauspost/compress
v1.17.4, x/net v0.23.0. Vendored + committed so the offline arm64 debian build
needs no network; debian/rules switched to -mod=vendor + GOTOOLCHAIN=local.

Tests: trusted cert round-trips; untrusted + hostname-mismatch rejected (no
plaintext) — verification boundary proven ON; HelloChrome asserted; h2-over-uTLS
dispatch; br/zstd round-trip + inject + fail-open + bomb cap; AE preserved
(present stays, absent stays absent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:01:05 +02:00
143e84f758 docs: record #662 post-cutover restore — ad metrics + popup CSS (PR #673) 2026-06-19 07:39:51 +02:00
CyberMind
1e76e70662
Merge pull request #673 from CyberMind-FR/feat/662-adstats-popups
feat(#662): restore ad-block metrics + popup-hiding CSS in the Go engine
2026-06-19 07:39:06 +02:00
4ea3f7b194 chore: changelog 0.1.5 (ref #662) 2026-06-19 07:35:15 +02:00
bf293eff2f harden(toolbox): bound /__toolbox/ad-event body size before parse (ref #662) 2026-06-19 07:35:15 +02:00
757bc292f9 feat(toolbox): restore ad-block metrics + popup-hiding CSS in Go MITM engine (ref #662)
The #662 cutover moved the BLOCK decision and the banner inject into the Go
engine (sbxmitm) but left two ad_ghost behaviours unported:

PART A — ad-block metrics (the #ads dashboard froze since the cutover):
  - new cmd/sbxmitm/adstats.go: lock-guarded in-memory aggregator keyed by
    (adHost,site) globally and (macHash,adHost) per-client; recordAdBlock()
    tallies 1 hit + 45000 bytes (matches the existing 45 KB/block convention),
    per-client only when macHash is set; size-capped at 5000 keys so a dead
    portal can't grow memory unbounded.
  - background flusher: snapshot-and-clear every 10s, POST best-effort to the
    portal /__toolbox/ad-event (fire-and-forget short-timeout client).
  - wired into mitmPipeline's verdict=="block" branch (site = registrable of
    the Referer host; per-client keyed on the WG persona hash).
  - new POST /__toolbox/ad-event ingest in the Python portal (api.py), beside
    the existing /__toolbox/loader.js + /__toolbox/bundle routes, same
    unauthenticated R3-perimeter trust; validates/caps rows, persists via
    store.record_ad_blocks / record_ad_client_blocks, never 500s the engine.

PART B — cosmetic / popup-ad hiding CSS:
  - new cmd/sbxmitm/cosmetic.go: ports the ad_ghost _COSMETIC selector set
    (ads / consent_nag / newsletter / social_widgets) and EXPANDS popup
    coverage with ad-SPECIFIC tokens (interstitial, popup-ad, popunder,
    exit-intent, ad-overlay, …). Conservative on purpose: NO bare generic
    modal/popup/overlay/lightbox tokens (they break legit UI).
  - injectCosmetic: idempotent, </head>/<head>/<body> placement mirroring
    injectLoader; gated to R3 (wg) clients and folded into injectIntoBody so
    it benefits from the same gzip decompress→inject→recompress path.

TDD: adstats_test.go + cosmetic_test.go cover aggregation, per-client gating,
snapshot-clear, payload shape, size-cap, idempotency, placement, selector
surface, the conservatism guard, and loader+cosmetic composition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 07:30:09 +02:00
381eb3b8f5 docs: WAF engine migration feasibility analysis (Coraza+CRS via HAProxy SPOA) (ref #662)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-18 22:31:12 +02:00
f9affe1e8b docs: record #662 Phase 7 — Python R3 decommissioned + nft persistence (epic complete) 2026-06-18 22:19:59 +02:00
eea4632642 fix(toolbox): persist R3 fanout to the Go engine ports 809x (was 808x Python) (ref #662)
The nft drop-in is what nftables.service re-applies at boot; pointing it at
the Go workers makes the #662 cutover survive a reboot. Rollback = 809x→808x.
Live /etc/nftables.d/zz-secubox-toolbox-wg-fanout.nft already updated + dry-run
validated (nft -c -f exit 0).
2026-06-18 22:08:37 +02:00
CyberMind
c7d354a153
Merge pull request #672 from CyberMind-FR/fix/662-no-follow-redirect
fix(#662): relay upstream redirects instead of following them
2026-06-18 22:02:09 +02:00
8e009e0aa6 fix(toolbox-ng): relay upstream 3xx instead of following them (ref #662) 2026-06-18 22:01:08 +02:00
CyberMind
e0cd433485
Merge pull request #671 from CyberMind-FR/fix/662-gzip-banner
fix(#662): inject banner into compressed HTML (gzip decode/re-encode)
2026-06-18 19:38:41 +02:00
8ffe54ee0d chore: changelog 0.1.3 — gzip banner inject (ref #662) 2026-06-18 19:37:43 +02:00
449b28f8a1 fix(toolbox-ng): inject banner into gzip HTML, not just identity (ref #662)
The Go MITM engine's transparency banner only appeared on UNCOMPRESSED
HTML. Browsers send `Accept-Encoding: gzip, br`, so most pages came back
gzip/brotli-compressed; the engine passed the compressed body straight
through and injectLoader (which scans for <head>/<body>) silently no-oped
on the binary blob. Proven on-board: identity HTML → banner present;
gzip HTML → banner absent.

Two-part fix, stdlib-only (compress/gzip; brotli/zstd are not in the
stdlib, which is why we constrain the wire to gzip):

1. mitmPipeline now pins the upstream request to `Accept-Encoding: gzip`
   (Set, not Del — Del would make Go's Transport auto-decompress and lose
   wire compression to the client for ALL resources). This guarantees
   every response is gzip or identity. Applies to both CONNECT and
   transparent paths (shared pipeline).

2. New gzip.go inject helper: in the existing 2xx + text/html gate,
   injectIntoBody gunzips → injectLoader → re-gzips when Content-Encoding
   is gzip (keeping the client transfer compressed), injects directly on
   identity, and fails open (original bytes untouched) on corrupt/unknown
   encoding or a decompression bomb (32MiB inflate cap). Content-Length /
   resp.ContentLength are updated to match the served bytes so the grown
   body is not truncated.

Non-HTML / non-2xx responses still pass through byte-for-byte (possibly
still gzip). Poison Set-Cookie + anonymize unchanged. Idempotency guard
stays inside injectLoader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:37:03 +02:00
4ef6d3aa76 docs: record #662 R3 cutover to Go engine + banner port (PR #670) 2026-06-18 19:20:21 +02:00
af76e33b45 docs: record #662 P5-prep + P6-prep (PRs #668, #669) in HISTORY 2026-06-18 19:19:38 +02:00
CyberMind
8df8f4d181
Merge pull request #670 from CyberMind-FR/feat/662-cutover-fix
feat(#662): R3 cutover to the Go MITM engine — unit fix, R3-CA loadCA, banner port
2026-06-18 19:19:23 +02:00
70d35eb7f2 feat(toolbox-ng): port real banner inject + /__toolbox portal reverse-proxy (ref #662) 2026-06-18 19:16:27 +02:00
73795bb3c3 feat(toolbox-ng): port transparency-banner loader inject + /__toolbox/* portal proxy (ref #662)
The Go MITM engine now injects the REAL visible transparency-banner loader
(replacing the invisible `<!-- sbx-ng banner -->` marker regression), mirroring
the authoritative Python inject_banner.py with stream_inject ON.

- banner.go: injectLoader() builds the guarded loader <script src="/__toolbox/
  loader.js" data-mh=.. data-wg=.. async> exactly like Python _loader_script;
  placement mirrors _LoaderInjector (after <head>'s '>', else before <body>,
  else unchanged); bannerGuard idempotency matches _GUARD; data-mh ascii-stripped.
- /__toolbox/loader.js + /__toolbox/bundle short-circuited in BOTH the CONNECT
  mitmPipeline and the transparent path, reverse-proxied to the portal
  (--portal, default http://127.0.0.1:8088). Startswith match (query-aware),
  fail-open to 204 so a banner asset never 502s the navigation.
- mitmPipeline threads `wg bool`: transparent path derives it from the
  10.99.1.0/24 peer IP (R3 WG), CONNECT passes false. Injection tightened to
  2xx text/html (Python skips non-200). injectMarker/Policy.Inject kept for the
  existing PoC tests.
- banner_test.go: guard idempotency, <head>/<body>/neither placement, wg + mh
  attributes, non-ascii stripping, path-detection + portal URL construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:12:28 +02:00
03fdc8fe14 fix(toolbox-ng): cutover-ready worker unit — live R3 CA, transparent 10.99.1.1 bind, combined-PEM loadCA (ref #662) 2026-06-18 18:54:41 +02:00
CyberMind
223f81ac63
Merge pull request #669 from CyberMind-FR/feat/662-transparent-machash
feat(#662 Phase 6-prep): transparent SO_ORIGINAL_DST accept + mac_hash persona (DARK)
2026-06-18 18:41:17 +02:00
bf022f618f fix(toolbox-ng): transparent upstream must verify cert against SNI, not bare IP (ref #662)
The transparent mitm/allow path set req.URL.Host = ip:port, so http.Client
TLS-dialed the captured original-dst and verified the cert against the bare IP
→ guaranteed SNI/cert-name mismatch. Add a per-request transparentTransport
whose DialContext pins the TCP dial to the captured ip:port for every
connection while TLSClientConfig.ServerName = the SNI host, so the upstream is
reached at the real IP yet the cert is verified by hostname. req.URL.Host now
carries the SNI host (correct Host header + SNI); verification stays ON (no
InsecureSkipVerify). The CONNECT path is unchanged (dialHost == "" → it still
dials by req.URL.Host exactly as before).
2026-06-18 18:37:35 +02:00
9df984c73f fix(toolbox-ng): transparent splice must not decrypt — peek+replay ClientHello (ref #662)
handleTransparent previously forged a cert and terminated TLS BEFORE Decide, so
a splice host was already MITM'd and the splice branch then io.Copy'd decrypted
plaintext into a cleartext dial — a broken relay of a host policy says to pass
through untouched (cert-pinned apps, own media infra).

Now: peek the ClientHello off the raw conn without consuming it (recordingReader
tees the bytes), parse SNI with a new pure stdlib sniFromClientHello (fully
bounds-checked, never panics), and Decide on the peeked SNI with NO decryption.
splice → dial the ORIGINAL dst, replay the buffered ClientHello upstream, pipe
raw TCP both ways, NEVER tls.Server. allow/mitm/block → re-present the buffered
ClientHello to tls.Server via a prefixConn (Read drains the prefix then
delegates) and run the shared pipeline as before.

Adds table tests for sniFromClientHello (hand-assembled ClientHello with/without
SNI, non-handshake, truncated, not-ClientHello → ("",false)), a no-panic
truncation sweep, and prefixConn replay tests.
2026-06-18 18:37:13 +02:00
5acfdb17c6 fix(toolbox-ng): non-linux build regression in transparent dispatch (ref #662)
main.go (untagged) referenced px.handleTransparent + the transparent accept
loop unconditionally, so the linux-only transparent.go made `GOOS=darwin
go build ./...` fail. Move the accept loop into a linux-tagged runTransparent
helper and add a non-linux transparent_stub.go that log.Fatals; main.go now
calls runTransparent only when --transparent. Verified GOOS=linux/arm64,
linux/amd64 and darwin all build.
2026-06-18 18:36:46 +02:00
364b8c4a30 feat(toolbox-ng): transparent SO_ORIGINAL_DST accept path (build only, DARK) (ref #662)
Add cmd/sbxmitm/transparent.go (//go:build linux): parseOrigDst decodes a raw
sockaddr_in/sockaddr_in6 blob (endianness-robust family, big-endian port) into
host:port — PURE, fully unit-tested. origDst recovers the pre-DNAT destination
via getsockopt(SO_ORIGINAL_DST=80) using syscall.Syscall6 on the raw fd
(stdlib-only). handleTransparent recovers origDst, terminates TLS by SNI,
splices raw TCP to the REAL captured dst or runs mitmPipeline dialling it.

transparent_test.go table-tests parseOrigDst (v4/v6, both family endiannesses,
BE port, short-blob errors). End-to-end getsockopt capture needs nft DNAT and
is validated at Phase 5 shadow on the board, not in unit tests (documented).
2026-06-18 18:24:57 +02:00
ba933a6ec3 refactor(toolbox-ng): extract shared post-TLS MITM pipeline + add --transparent flag (ref #662)
Factor handleConnect's post-handshake logic (read request, apply verdict,
anonymize, proxy upstream, poison, inject, write) into mitmPipeline so the
CONNECT and transparent accept paths can't drift. dialHost param lets the
transparent path dial the captured original-dst instead of the SNI. Add a
--transparent bool flag: when set, a raw net.Listen accept loop dispatches each
conn to handleTransparent; default keeps the CONNECT http.Server EXACTLY.
CONNECT path + its tests unchanged.
2026-06-18 18:24:57 +02:00
67e85ba4dd feat(toolbox-ng): wire mac_hash into clientHashFromConn + Python parity (ref #662)
clientHashFromConn now resolves the peer IP via macHashOf (WG persona hash,
byte-identical to Python for 10.99.1.0/24), falling back to the raw peer IP for
non-WG/test conns so poison stays deterministic. Updated the TODO block: WG
mac_hash wiring DONE; remaining gap is only the transparent original-dst
plumbing (Deliverable 2) and the intentionally-out-of-scope R0-R2 ARP path.

test_machash_parity.py drives _common.mac_hash_of on the SAME fixtures; both
engines agree. Anti-rig verified on the Python side too.
2026-06-18 18:21:45 +02:00
5fb67f5b88 feat(toolbox-ng): port WG persona mac_hash to Go with cross-engine parity (ref #662)
Port _common._wg_hash_of / mac_hash_of to cmd/sbxmitm/machash.go: WG peers on
10.99.1.0/24 resolve to sha256(peer_pubkey)[:16], mtime-cached behind a mutex
(Go is concurrent; Python relied on the GIL). Off-subnet / R0-R2 ARP path is
out of scope for the R3 transparent engine; any error fails open to "".

Parity fixtures (testdata/wg-peers-fixture.json + machash-fixtures.json) carry
Python-authored expected values; machash_test.go asserts macHashOf matches.
Anti-rig verified: [:16]->[:15] fails the test.
2026-06-18 18:21:39 +02:00
CyberMind
c870b6362b
Merge pull request #668 from CyberMind-FR/feat/662-phase5prep-pkg
feat(#662 Phase 5-prep): wire Decide+jar+anonymize+poison into handlers + DARK debian package
2026-06-18 18:06:31 +02:00
de15a18c30 feat(#662 Phase 5-prep B): debian packaging for sbxmitm (DISABLED, dark)
New packages/secubox-toolbox-ng/debian/ producing the secubox-toolbox-ng
binary package (Architecture: arm64):
  - control: Maintainer Gerald KERMA; B-D golang-go; Depends
    only (static CGO_ENABLED=0 binary → no shlib deps). Compat 13 via
    debhelper-compat build-dep (debhelper rejects compat both ways).
  - changelog: 0.1.0-1~bookworm1.
  - rules: dh; GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GOPROXY=off go build
    (pure stdlib, offline). dh_installsystemd --no-enable --no-start so the
    unit is shipped but NEVER enabled/started.
  - secubox-toolbox-ng-worker@.service: systemd template mirroring the Python
    mitm-wg worker@ but running sbxmitm on 127.0.0.1:809%i (distinct from the
    Python 808%i so both fleets coexist during cutover). Reads the ca-wg CA.
    DISABLED BY DESIGN — header documents Phase-6-cutover-only enablement.
  - postinst: daemon-reload only; explicitly NO enable/start; NO nft.

Built locally for arm64: dpkg-deb verified — ships /usr/sbin/sbxmitm (arm64
static ELF) + the disabled template; postinst contains ZERO deb-systemd-helper
enable lines. .gitignore extended for in-tree build artifacts. DARK: install
changes no runtime behaviour (no service start, no DNAT, no live-R3 wiring).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:03:40 +02:00
f65af3355c feat(#662 Phase 5-prep A): wire ported policy+jar into proxy handlers
Replace the hardcoded action() short-circuit in handleConnect with the
ported Decide(host,sni) + always-on anonymize + Set-Cookie poison:

  - allow/own-infra  → clean MITM (anonymize only, NO block/poison)
  - splice           → raw passthrough (unchanged)
  - block            → 204 (unchanged)
  - mitm + tracker   → poison tracking-id Set-Cookies via the HMAC jar

New pure, unit-testable helpers (privacy.go):
  - anonymizeRequest(http.Header): drop operator/carrier + re-id headers
    (mirrors privacy_guard._STRIP), pin DNT:1 + Sec-GPC:1.
  - isTrackingCookieName / poisonSetCookies: replace tracking-id cookie
    values with fakeID(clientHash,host,name,jarKey); attrs preserved,
    benign cookies untouched, fail-closed-to-clean when no key/clientHash.
  - Policy.isTracker / Policy.shouldPoison: poison ONLY on MITM'd tracker
    flows, never on allow/own-infra (same dark safety as the block path).
  - clientHashFromConn: PoC peer-IP stub, TODO(#662 P6) mac_hash via
    SO_ORIGINAL_DST + WG-peer map.

writeResponse (util.go) preserves multi-valued Set-Cookie headers.
Poison gated behind --poison (default on) AND a loaded --jar-key.
DARK: nothing wired to live R3. +8 tests (14→22), all green; vet clean;
arm64 cross-build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:00:26 +02:00
CyberMind
7355e606ca
Merge pull request #667 from CyberMind-FR/feat/662-phase4-jar
feat(#662 Phase 4): anti-track HMAC jar port (byte-exact) + sidecar emit
2026-06-18 17:52:12 +02:00
e594f681a4 doc(#662 Phase 4): fix stale fakeID comment — clarify it MUST use registrableJar (anti-consolidation footgun guard) 2026-06-18 17:52:03 +02:00
0db96a8beb fix(#662 Phase 4): jar uses privacy-flavored registrableJar (not ad_ghost) — byte-parity on gov.uk/IP trackers; + divergence-guard fixtures 2026-06-18 17:49:05 +02:00
667d8a09e0 feat(#662 Phase 4): sidecar emit helper (fire-and-forget unix-socket POST)
Add sidecar.go (package main, stdlib only): emit(socketPath, route, payload)
relays a signal to a SecuBox module's unix socket in a detached goroutine —
never blocks the proxy flow, never raises into the caller (mirrors
_common.fire_forget_post + queue_async). emitSync is the same-package,
test-observable synchronous form under a 2s timeout (mirrors httpx timeout=2).

Documents the addon→socket mapping the live engine will use
(cookies/dpi/avatar/ja4/soc_relay → /run/secubox/*.sock; social_graph is
in-process). NOT wired into the live path — transport only (Phase 5+ wiring).

sidecar_test.go: delivery over a throwaway unix socket, dead-socket
no-panic/no-block, empty-route defaulting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:45:52 +02:00
170619053f feat(#662 Phase 4): port anti-track HMAC fake-identity jar to Go, byte-exact
Port privacy.py's _jar_key / _shape / fake_id into jar.go (package main,
stdlib only): loadJarKey (read+TrimSpace, empty->nil), shape (GA1/fb uint64
big-endian modulo math, uuid via rune-length >=32 branch, hex[:32] default),
fakeID (HMAC-SHA256 over client|registrable(tracker)|cookie, reuses the
Phase-3 registrable()). Returns ("",false) where Python returns None.

Cross-engine parity proven: testdata/jar-fixtures.json (expect values
GENERATED by privacy.fake_id with a FIXED test key, not the real /etc key)
covers _ga, _ga_<prop> GA4, _fbp, uuid, _pk_id, name>=32, generic hex, and a
doubleclick.net subdomain-folding case. jar_test.go (Go) and
tests/test_jar_parity.py (Python) load the SAME fixtures+key and both pass ->
byte-exact. No int-math or rune-length divergence found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:44:14 +02:00
CyberMind
25f6c19586
Merge pull request #666 from CyberMind-FR/feat/662-phase3-parity
feat(#662 Phase 3): Go block/splice decision engine + cross-engine parity harness
2026-06-18 17:38:07 +02:00
6dcf978e66 test(#662 Phase 3): harden parity harness — own-infra false-prefix + comment-kept learned-trackers fixtures (lock the loadLinesRaw vs loadLines divergence) 2026-06-18 17:37:45 +02:00
df052796d9 test(#662 Phase 3): cross-engine parity harness — Python side (source of truth)
tests/test_engine_parity.py loads the SAME parity-fixtures.json + testdata
config from ../secubox-toolbox-ng/testdata, monkeypatches ad_ghost paths
(_ALLOW_PATH/_LEARNED_PATH/_SELF_REGS + cache resets) at the snapshot, and
drives the production decision logic — ad_ghost._allowed + _AD_HOST + the
learned-trackers check composed with splice.should_splice — under the SAME
precedence as Go's Decide. Asserts action == expect for every fixture.

Parity proven: this run caught a real Go↔Python divergence — Go had
comment-stripped learned-trackers, but ad_ghost._learned_set does not; Go was
fixed (loadLinesRaw) to match Python. Python is the source of truth.

test_fixtures_present guards that all four action classes are exercised.
2026-06-18 17:32:38 +02:00
5fc8785d68 test(#662 Phase 3): cross-engine parity harness — Go side + fixtures
testdata/parity-fixtures.json + testdata/config/ snapshot: a FIXED config both
engines load identically. Fixtures cover every action class — static ad host,
learned-tracker, pure-tracker that is also a splice candidate (never wins →
block), own-infra secubox.in subdomain (allow), allowlisted host (allow),
splice-seed + splice-learned hosts (splice), fortknox site in never-set (mitm),
no-false-suffix negative (notdoubleclick.net → mitm), plain site (mitm).

policy_test.go: TestParityDecide loads the fixtures+config and asserts
Decide == expect; TestPolicyActionVerbs checks the legacy action() surface;
TestRegistrable exercises the _registrable port incl. 2-level TLDs.
2026-06-18 17:32:31 +02:00
25a3afaff1 feat(#662 Phase 3): port toolbox BLOCK/SPLICE logic into Go core
Add cmd/sbxmitm/policy.go: a LoadPolicy() layer that reads the SAME on-disk
config the Python addons use (ad-allowlist.txt, learned-trackers.txt,
tls-splice-seed.conf, splice-learned.txt, pure-trackers.txt) with the same
env overrides, plus a unified Decide(host, sni) -> {allow,block,splice,mitm}.

Ports, byte-for-byte against the Python source of truth:
  - _AD_HOST regex (RE2-safe → Go (?i) inline flag, no fallback needed)
  - _registrable incl. the _2L two-level-TLD list
  - splice.host_matches / should_splice (never wins; then seed∪learned)
  - ad_ghost._allowed (own-infra + allowlist ALWAYS win first)

Loader nuance preserved: ad_ghost._learned_set does NOT comment-strip
(machine-generated file), unlike the splice/allowlist loaders — mirrored via
loadLinesRaw vs loadLines so a '#' in learned-trackers is kept verbatim.

Decide precedence: allow > splice (never-set excludes trackers) > block > mitm.

Wire the loaded policy into the PoC CONNECT proxy (replacing the hardcoded
AdHosts/SpliceHosts); action() keeps the legacy 3-verb surface (allow→mitm).
Old TestActionDecision removed (drove the removed hardcoded fields); coverage
moves to the parity harness.
2026-06-18 17:32:20 +02:00
CyberMind
84f0a37fdf
Merge pull request #665 from CyberMind-FR/feat/662-phase2b-bench
feat(#662 Phase 2b): multi-core throughput bench (3.4x at 4 cores)
2026-06-18 17:23:35 +02:00
ca9b38b175 feat(#662 Phase 2b): parallel handshake bench — Go core scales 3.4x at 4 cores (multi-core gate settled) 2026-06-18 17:23:23 +02:00
8a4996d14c docs(#662): Phase 2 bench results — Go PoC proven on arm64 (CA-compat/204/inject/JA4/12MB); throughput gate deferred to controlled bench 2026-06-18 17:19:01 +02:00
CyberMind
da71515d79
Merge pull request #664 from CyberMind-FR/fix/662-restore-ng-source
fix(#662): restore Go PoC source lost to .gitignore
2026-06-18 17:13:42 +02:00
73e79b85b4 fix(#662): restore Go PoC source — .gitignore 'sbxmitm' wrongly ignored cmd/sbxmitm/ dir (anchored to /sbxmitm) 2026-06-18 17:13:28 +02:00
CyberMind
56d1bee9fb
Merge pull request #663 from CyberMind-FR/feature/662-epic-migrate-toolbox-mitm-engine-off-pyt
epic: migrate toolbox MITM engine off Python mitmproxy (gomitmproxy/hudsucker/Squid analysis + phased switch)
2026-06-18 17:09:26 +02:00
6daacb1987 feat(#662 Phase 1): MITM-engine migration analysis + phased plan + compiled/tested Go forging-MITM PoC
Analysis: gomitmproxy (unmaintained, dropped) vs martian/goproxy (Go) vs hudsucker
(Rust) vs Squid+ICAP, mapped to the 18-addon capability set. Recommendation: Go
hot-path core + retained Python analysis sidecars. Phased plan with shadow-run +
nft-DNAT-flip rollback (no big-bang cutover). Phase-1 PoC (packages/secubox-toolbox-ng,
stdlib-only): forge from ca-wg CA, 204-block, body-inject, SNI-splice, ClientHello/JA4
capture — go vet clean, tests green, arm64 cross-compile OK. NOT wired to live R3.
2026-06-18 17:07:48 +02:00
CyberMind
69f19e72da
Merge pull request #661 from CyberMind-FR/fix/cap-lists-5
Some checks are pending
License Headers / check (push) Waiting to run
ui(toolbox): cap all dashboard lists to 5 shown
2026-06-18 14:49:59 +02:00
be7704bc1a ui(toolbox): cap all admin dashboard lists to top-5 shown (#filtres/#social/#ads) 2026-06-18 14:49:35 +02:00
CyberMind
051ca6d1d7
Merge pull request #660 from CyberMind-FR/feature/659-feat-toolbox-per-visitor-ad-block-breakd
feat(toolbox): per-visitor ad-block breakdown in #ads (top visitors + per-visitor drill-down)
2026-06-18 14:13:41 +02:00
69659f6a67 chore(toolbox): changelog 2.6.58 for per-visitor ad breakdown (ref #659) 2026-06-18 14:13:16 +02:00
1bd5108472 feat(toolbox #659): #ads tab — top visitors + per-visitor drill-down
renders d.top_visitors into #ads-visitors; clickable mac_hash calls
loadAdsClient → /admin/ad-stats/client/{mac_hash} into #ads-client-detail.
all values escaped.
2026-06-18 14:10:00 +02:00
6c96ba62e4 feat(toolbox #659): GET /admin/ad-stats/client/{mac_hash} drill-down
per-visitor ad-block detail endpoint, hours clamped 1..168.
2026-06-18 14:09:22 +02:00
4d0cbf8b7f feat(toolbox #659): ad_ghost accumulates blocked ads per visitor
guarded mac_hash_of import; _cli hot-path dict; per-visitor tally on the
204 block branch; drained + offloaded to record_ad_client_blocks in _flush.
hot path stays in-memory increments only.
2026-06-18 14:08:59 +02:00
4f96da87d7 feat(toolbox #659): store per-visitor ad-block breakdown
ad_block_client_host table + record_ad_client_blocks upsert +
ad_client_stats drill-down; ad_stats now returns top_visitors.
2026-06-18 14:07:02 +02:00
CyberMind
2b036db0d6
Merge pull request #658 from CyberMind-FR/fix/ad-learn-self-block-hardening
fix(toolbox #658): ad-learn never self-blocks own infra + exact-host promotion
2026-06-18 13:55:47 +02:00
376b4ecd2a fix(toolbox #658): ad-learn never self-blocks own infra + exact-host promotion (no registrable over-fold) 2026-06-18 13:53:42 +02:00
a3cd643da4 docs: HISTORY — #656 Ad Intelligence shipped + splice/banner reverted 2026-06-18 13:33:49 +02:00
CyberMind
f9e2032750
Merge pull request #657 from CyberMind-FR/feature/656-feat-toolbox-ad-intelligence-aggressive
feat(toolbox): Ad Intelligence — aggressive ad-URL learning + block/silent/drop + contextual #ads metrics
2026-06-18 13:21:01 +02:00
e39123dc7e fix(toolbox): allow #ads deep-link to auto-open the tab on load (ref #656) 2026-06-18 13:20:26 +02:00
d5408536b3 chore(toolbox): changelog 2.6.56 for Ad Intelligence (ref #656) 2026-06-18 13:17:46 +02:00
12277f3c08 feat(toolbox): #ads dashboard tab — contextual ad-block stats (ref #656) 2026-06-18 13:16:48 +02:00
976db154a0 feat(toolbox): autolearn _ad_feed promotes ad-candidates to blocklist (ref #656) 2026-06-18 13:15:45 +02:00
d6dda15024 feat(toolbox): ad_ghost contextual recording + candidates + allowlist (ref #656) 2026-06-18 13:14:01 +02:00
fa1ad79239 test(toolbox): modernize emoji test to asyncio.run (order-independent, fixes flake) (ref #656) 2026-06-18 13:11:36 +02:00
e23fa9545e feat(toolbox): GET /admin/ad-stats contextual metrics (ref #656) 2026-06-18 13:09:39 +02:00
96836d865c feat(toolbox): ad_learn filter toggle (ref #656) 2026-06-18 13:08:36 +02:00
3243e0d2f2 feat(toolbox): ad_block_stats + ad_candidates store helpers (ref #656) 2026-06-18 13:08:01 +02:00
3dfe85f547 docs: implementation plan for Ad Intelligence (#656) 2026-06-18 13:06:26 +02:00
221abd94a2 docs: spec for Ad Intelligence — learn/act/measure + #ads tab (#656) 2026-06-18 13:04:53 +02:00
ebb6b3067c Revert "Merge pull request #654 from CyberMind-FR/fix/653-fix-toolbox-banner-reliability-inline-bu"
This reverts commit 6160ec9aaa, reversing
changes made to f69384f1e0.
2026-06-18 12:23:03 +02:00
CyberMind
6160ec9aaa
Merge pull request #654 from CyberMind-FR/fix/653-fix-toolbox-banner-reliability-inline-bu
fix(toolbox): banner reliability — inline bundle (kill connect-src fetch) + SPA re-assert
2026-06-18 12:14:33 +02:00
f30b3b39f8 fix(toolbox #653): inline bundle into loader (kill connect-src fetch, faster) + SPA re-assert 2026-06-18 12:11:47 +02:00
CyberMind
f69384f1e0
Merge pull request #652 from CyberMind-FR/feature/651-perf-toolbox-649-follow-up-broaden-splic
perf(toolbox): #649 follow-up — broaden splice seed + trustworthy per-worker stats + admin toggle, flip on
2026-06-18 11:53:01 +02:00
56baace9d1 fix(#651): narrow risky seed entries to media shards before arming (imgur/giphy/redd.it/pstatic apex → media subdomains; drop akamaihd) 2026-06-18 11:52:20 +02:00
53df892193 perf(toolbox #651): broaden media splice seed (35) + per-worker stats + console feedback 2026-06-18 11:50:25 +02:00
1139ce103e docs: record #649 selective SNI-splice (Lever A) ship + soak/flip next (PR #650) 2026-06-18 11:30:02 +02:00
CyberMind
173b67c495
Merge pull request #650 from CyberMind-FR/feature/649-perf-toolbox-selective-sni-splice-passth
perf(toolbox): selective SNI-splice — passthrough pure-asset flows, MITM only what needs L7 (Lever A)
2026-06-18 11:26:59 +02:00
e8cebe1662 fix(toolbox): refresh never-set on filters.json mtime too (fortknox via WebUI) (ref #649) 2026-06-18 11:26:35 +02:00
3b0423189e feat(toolbox): register tls_splice first in mitm-wg chain + changelog 2.6.54 (ref #649) 2026-06-18 11:23:59 +02:00
742897700c perf(toolbox): offload splice obs to bg thread + skip decided hosts; add fail-safe/recorder tests (ref #649) 2026-06-18 11:22:47 +02:00
296cebc69e feat(toolbox): autolearn promotes never-HTML hosts to splice-learned (ref #649) 2026-06-18 11:18:36 +02:00
4160167e5c feat(toolbox): tls_splice addon — SNI-splice at ClientHello + obs recorder (ref #649) 2026-06-18 11:17:39 +02:00
d836179a72 feat(toolbox): splice_host_obs table + record/never_html helpers (ref #649) 2026-06-18 11:16:39 +02:00
9d3630574f feat(toolbox): curated media SNI-splice seed (ref #649) 2026-06-18 11:11:43 +02:00
8664c84893 feat(toolbox): SNI-splice classifier (seed/learned/never) (ref #649) 2026-06-18 11:11:30 +02:00
96e04bbe0f feat(toolbox): tls_splice filter toggle off|observe|on (ref #649) 2026-06-18 11:10:53 +02:00
09e16f35a1 docs: implementation plan for toolbox selective SNI-splice (#649) 2026-06-18 11:09:02 +02:00
7834a29724 docs: spec for toolbox selective SNI-splice (Lever A, #649) 2026-06-18 11:04:26 +02:00
ab8822e3f4 docs: record #623 systemic shared-parent clobber fix (PR #648) 2026-06-18 10:48:24 +02:00
CyberMind
9950e9ec3e
Merge pull request #648 from CyberMind-FR/fix/623-postinst-systemic-var-lib-log-cache-secu
postinst: systemic /var/{lib,log,cache}/secubox parent-mode clobber across ~12 packages (board-wide traversal breakage)
2026-06-18 10:41:52 +02:00
951af764fb fix(#623): close review gaps — 3 chmod 750 /var/log clobbers + tmpfiles defense-in-depth
Review found two gaps the install-d sweep missed:
- chmod-form clobber: soc-gateway/soc-agent/ui-manager did 'chmod 750
  /var/log/secubox' (same #511 traversal class) -> now 0755.
- secubox-core tmpfiles.d only declared /run/secubox; now declares all 5 shared
  parents at 0755 (mode-only, owner-agnostic) for boot/install-time self-heal.
  core 1.1.7 -> 1.1.8.
2026-06-18 10:39:52 +02:00
6b7d7f8607 fix(#623): shared /run|/var/lib|/var/cache|/etc/secubox parents stay 1777/0755 in all postinsts
Systemic clobber: the scaffold boilerplate (install -d -m 750 /var/lib/secubox,
/run/secubox) put restrictive modes on SHARED parents in ~56 module postinsts,
reverting them to 0750 on every install/upgrade and breaking traversal for
non-secubox daemons (kbin/toolbox 500). Empirically confirmed install -d -m only
modes the final component, so /parent/leaf forms are harmless — only bare-parent
targets were rewritten. Multi-arg lines (incl. ones making /var/lib world-writable
1777) split per-parent: /run/secubox=1777 root:root, /var/lib|cache|etc=0755
secubox:secubox; module-private leaves keep 0750. Scaffold + PATTERNS.md fixed so
new packages don't reintroduce it.
2026-06-18 10:32:58 +02:00
68490a4a9b docs: update WIP + HISTORY for #644/#646 perf sprint + crowdsec unblock 2026-06-18 10:18:01 +02:00
CyberMind
a44d9c51d8
Merge pull request #647 from CyberMind-FR/feature/646-perf-toolbox-adaptive-accept-encoding-st
perf(toolbox): adaptive Accept-Encoding strip — stop pulling CSP-strict pages uncompressed through R3
2026-06-18 09:34:00 +02:00
013385a6c4 chore(toolbox): changelog 2.6.53 for adaptive Accept-Encoding strip (ref #646) 2026-06-18 09:31:50 +02:00
6ee7fe3cbc perf(toolbox): adaptive Accept-Encoding strip — only force identity on stream-eligible hosts (ref #646) 2026-06-18 09:31:29 +02:00
CyberMind
2f9b16f05a
Merge pull request #645 from CyberMind-FR/fix/644-hub-dashboard-latency-9-12s-per-module-s
hub dashboard latency: 9-12s (per-module systemctl) + health-batch 3.3s (uncached); clients/rich 180ms
2026-06-18 09:01:41 +02:00
ce571233d7 perf(hub): double-checked locking on cold cache refresh — collapse thundering herd (ref #644) 2026-06-18 09:01:04 +02:00
89b962aa8c chore: changelogs for hub latency + toolbox clients/rich cap (ref #644) 2026-06-18 08:57:47 +02:00
d8edf050f5 perf(toolbox): cap /admin/clients/rich enrichment to ENRICH_LIMIT most-recent rows (ref #644) 2026-06-18 08:56:13 +02:00
7349288e88 perf(hub): warm services cache on /alerts too — last cold _svc consumer (ref #644) 2026-06-18 08:54:24 +02:00
3ec9a82984 perf(hub): serve dashboard/health-batch from cache, one batched systemctl on cold path (ref #644) 2026-06-18 08:52:28 +02:00
1a87993394 perf(hub): add _ensure_services_warm + _refresh_health_batch cache helpers (ref #644) 2026-06-18 08:47:55 +02:00
7dd39d52a4 docs: spec + plan for hub dashboard latency fix (ref #644) 2026-06-18 08:45:31 +02:00
CyberMind
9d10dd5aea
Merge pull request #643 from CyberMind-FR/fix/642-social-graph-ip-literal-self-traffic-rec
Some checks are pending
License Headers / check (push) Waiting to run
social IP-literal fix. Closes #642
2026-06-17 17:04:02 +02:00
df864f2a99 chore(toolbox): changelog 2.6.51 for social IP-literal fix (ref #642) 2026-06-17 17:03:38 +02:00
a6d4cc1632 fix(toolbox): social drops IP-literal edges + total_trackers_seen excludes IPs (KPI matches table) (ref #642) 2026-06-17 17:01:24 +02:00
ea19e80685 docs: #642 social IP-literal fix design + plan (ref #642) 2026-06-17 16:58:28 +02:00
CyberMind
2b05a31ab2
Merge pull request #641 from CyberMind-FR/feature/634-toolbox-clients-reset-all-clients-reset
Some checks are pending
License Headers / check (push) Waiting to run
#clients reset-all + device/geo emojis. Closes #634, #635
2026-06-17 16:19:30 +02:00
626acede56 chore(toolbox): changelog 2.6.50 for #clients reset-all + emojis (ref #634, #635) 2026-06-17 16:15:33 +02:00
2f3785d7fe fix(toolbox): resetAllClients single error path (surface server 403 detail) (ref #634) 2026-06-17 16:15:06 +02:00
8c932b1b2b feat(toolbox): #clients UI — device/flag/hosting + Reset-all button (ref #634, #635) 2026-06-17 16:12:01 +02:00
bf0df9b0a1 feat(toolbox): clients/rich device emoji (UA) + country flag + hosting (geo) (ref #635) 2026-06-17 16:08:18 +02:00
c536d71657 feat(toolbox): POST /admin/clients/reset-all (bulk per-client reset, kbin-gated) (ref #634) 2026-06-17 16:04:29 +02:00
8f8dfb137c feat(toolbox): store.latest_user_agent for client device detection (ref #635) 2026-06-17 16:02:05 +02:00
765de07ac8 docs: #clients reset-all + emojis implementation plan (ref #634, #635) 2026-06-17 16:01:03 +02:00
c687225e1b docs: #clients reset-all (#634) + device/geo emojis (#635) design (ref #634) 2026-06-17 15:58:46 +02:00
CyberMind
2f6ca5478b
Merge pull request #640 from CyberMind-FR/fix/639-toolbox-banner-injected-into-iframes-sub
Top-level-only banner gate. Closes #639
2026-06-17 15:41:04 +02:00
1d4b137dad chore(toolbox): changelog 2.6.49 for #639 top-level banner gate (ref #639) 2026-06-17 15:40:32 +02:00
90512da938 fix(toolbox): inject banner only into top-level documents (skip iframes) — one banner per visit (ref #639) 2026-06-17 15:38:49 +02:00
981acc4b44 docs(wiki): add Anti-Track v2 page + sidebar entry 2026-06-17 15:15:28 +02:00
bd54d82ae2 docs: anti-tracking v2 design — layered block/poison/anonymize 2026-06-17 15:15:28 +02:00
CyberMind
abba571443
Merge pull request #638 from CyberMind-FR/fix/636-toolbox-r3-banner-stream-inject-loader-b
R3 banner fix — mitm serves /__toolbox/*, CSP fallback, top bar, cache key. Closes #636
2026-06-17 15:13:58 +02:00
30eeedf86e Merge remote-tracking branch 'origin/master' into fix/636-toolbox-r3-banner-stream-inject-loader-b
# Conflicts:
#	packages/secubox-toolbox/debian/changelog
2026-06-17 15:13:36 +02:00
CyberMind
50c4ea59d0
Merge pull request #637 from CyberMind-FR/feature/633-anti-track-v2-layered-block-poison-anony
Anti-Track v2 — Plans 1/2a/2b-IP/2b-DNS/2c/2d (dark). Closes #633
2026-06-17 15:12:30 +02:00
15898cbb9d chore(toolbox): changelog for Anti-Track v2 Plan 2d (ref #633) 2026-06-17 15:09:58 +02:00
bf64995eda docs(toolbox): #636 changelog covers loader-serving keystone + top bar (ref #636) 2026-06-17 15:03:13 +02:00
a2e7f1103d feat(toolbox): banner loader renders top bar + body padding (operator preference) (ref #636) 2026-06-17 15:01:30 +02:00
9f47542e2f docs(toolbox): explain startswith + trust boundary on /__toolbox interception (ref #636) 2026-06-17 15:00:00 +02:00
65da37a486 fix(toolbox): mitm serves /__toolbox/loader.js + /__toolbox/bundle for any origin (R3 banner fix) (ref #636) 2026-06-17 14:56:37 +02:00
9094b202c9 feat(toolbox): #social shows top-5 trackers + tout-afficher toggle, escape domain (ref #633) 2026-06-17 14:47:34 +02:00
555176fef5 docs: Anti-Track v2 Plan 2d implementation plan (ref #633) 2026-06-17 14:46:32 +02:00
572caab035 docs: Anti-Track v2 Plan 2d #social top-5 design (ref #633) 2026-06-17 14:45:36 +02:00
00740feb43 chore(toolbox): changelog for #636 banner CSP fallback (ref #636) 2026-06-17 14:41:38 +02:00
e822167a49 fix(toolbox): bundle cache keyed by (client_id, is_wg) so wg/non-wg report_url don't bleed (ref #636) 2026-06-17 14:39:32 +02:00
1c26ad6bbd fix(toolbox): banner stream_inject falls back to buffer path on strict CSP (ref #636) 2026-06-17 14:35:19 +02:00
6694860779 docs: #636 implementation plan (CSP fallback + bundle cache key) (ref #636) 2026-06-17 14:33:50 +02:00
876becec0a docs: #636 amend — retract Bug2 (R3 report-url correct); real residual is bundle cache key (ref #636) 2026-06-17 14:32:49 +02:00
644996b9f5 docs: #636 R3 banner CSP-fallback + report-url fix design (ref #636) 2026-06-17 14:30:43 +02:00
56241026e1 chore(toolbox): changelog for Anti-Track v2 Plan 2c (ref #633) 2026-06-17 14:20:34 +02:00
c4f60fd166 fix(toolbox): HTML-escape bypass host/source in #filtres render (stored-XSS hardening) (ref #633) 2026-06-17 14:19:36 +02:00
f3afa4dc2b feat(toolbox): #filtres panel source badges + legend (seed/static/learned) (ref #633) 2026-06-17 14:17:22 +02:00
6b111adb18 feat(toolbox): merge package bypass seed into ignore_hosts (3-way) (ref #633) 2026-06-17 14:15:09 +02:00
3370d9bfbb fix(toolbox): remove duplicate stale /admin/filter-control/list route (wrong shape) (ref #633) 2026-06-17 14:12:38 +02:00
751195d8fc feat(toolbox): add tagged /admin/filter-control/list endpoint for #filtres panel (ref #633) 2026-06-17 14:10:05 +02:00
089c4791fe refactor(toolbox): drop dead _MITM_BYPASS_DEFAULT_ENTRIES (seed now in conf file) (ref #633) 2026-06-17 14:08:16 +02:00
5113fe1e60 feat(toolbox): package-owned cert-pinned bypass seed; operator file starts empty (ref #633)
- Add conf/mitm-bypass-seed.conf with all 37 cert-pinned bypass patterns
  extracted verbatim from _MITM_BYPASS_DEFAULT_ENTRIES (Signal, WhatsApp,
  Telegram, Apple Push, French banks, Google GMS, Meta/Facebook, ad networks).
- _ensure_bypass_file() now creates an EMPTY operator file (mitm-bypass.conf)
  on first boot instead of writing the defaults there; seed is package-owned
  and read-only at /usr/lib/secubox/toolbox/conf/mitm-bypass-seed.conf.
- Add MITM_BYPASS_SEED_FILE and MITM_BYPASS_DYNAMIC_FILE Path constants
  (env-overridable for tests). _MITM_BYPASS_DEFAULT_ENTRIES kept in place
  (Task 2 will supersede its use).
- Add tests/test_bypass_sources.py (TDD: seed exists, ≥15 patterns, regex
  alternation compiles and matches expected hosts).
- Seed ships automatically via existing cp -r conf in debian/rules (no
  debian/rules change needed).
2026-06-17 14:03:47 +02:00
abc5dd47cb docs: Anti-Track v2 Plan 2c implementation plan (ref #633) 2026-06-17 14:01:21 +02:00
aac1ae58d9 docs: amend Plan 2c spec — missing /list endpoint is the real bug; seed exists inline (ref #633) 2026-06-17 13:59:08 +02:00
0c0c592b0d fix(toolbox): DNS-safe charset guard on unbound zone names (prevent malformed drop-in / DNS outage) (ref #633) 2026-06-17 13:57:23 +02:00
9fa3c9ff0d chore(toolbox): changelog for Anti-Track v2 Plan 2b-DNS (ref #633) 2026-06-17 13:53:11 +02:00
095d5b9da1 fix(toolbox): fully crash-isolate _dns_feed (wrap block-line render) (ref #633) 2026-06-17 13:52:10 +02:00
82aa9c3def feat(toolbox): autolearn writes unbound NXDOMAIN drop-in for pure trackers (dark-gated) (ref #633) 2026-06-17 13:49:05 +02:00
3915f6ea41 feat(toolbox): filters.json path env-overridable (SECUBOX_FILTERS_PATH) (ref #633) 2026-06-17 13:46:02 +02:00
23ce1c16ba feat(toolbox): ip_dns unbound NXDOMAIN drop-in formatter (ref #633) 2026-06-17 13:43:22 +02:00
fea1147bf6 docs: Anti-Track v2 Plan 2b-DNS implementation plan (ref #633) 2026-06-17 13:41:59 +02:00
31dc83f696 docs: Anti-Track v2 Plan 2b-DNS design (unbound NXDOMAIN, live-verified topology) (ref #633) 2026-06-17 13:39:57 +02:00
cfdb1c418c docs: Anti-Track v2 Plan 2c bypass-seed design (ref #633) 2026-06-17 13:33:56 +02:00
41fb5e6102 feat(toolbox): per-IP audit for pure-tracker drops (CSPN forensics) (ref #633)
Replace aggregate-count audit with individual IP list in the audit line so
each enforcement decision is independently traceable. Add CDN-allowlist
fail-open comment. Test now asserts the dropped IP appears in the audit entry.
2026-06-17 13:14:07 +02:00
d8c34b2811 chore(toolbox): changelog for Anti-Track v2 Plan 2b IP-drop (ref #633) 2026-06-17 13:10:10 +02:00
10565ad4e9 chore(toolbox): escalate gated block uses relative import, drop dead sys.path (ref #633) 2026-06-17 13:09:09 +02:00
cb21023e4d feat(toolbox): escalate drops exclusive pure-tracker IPs (dark-gated, CDN-safe) (ref #633) 2026-06-17 13:05:39 +02:00
833905a1dd feat(toolbox): ship CDN/cloud allowlist for exclusive-IP gate (ref #633) 2026-06-17 13:01:15 +02:00
64c547aeea feat(toolbox): ip_dns exclusive-tracker-IP computation (CDN-gated) (ref #633) 2026-06-17 12:58:30 +02:00
f3dc9e9bb2 chore(toolbox): warn when CDN allowlist missing (fail-open visibility) (ref #633) 2026-06-17 12:57:29 +02:00
80dc9e6c06 feat(toolbox): ip_dns CDN allowlist parsing + membership (ref #633) 2026-06-17 12:55:42 +02:00
78a049c4b2 docs: Anti-Track v2 Plan 2b IP-drop implementation plan (ref #633) 2026-06-17 12:52:41 +02:00
ee3a9aee4d docs: Anti-Track v2 Plan 2b enforcement design (DNS-refuse + exclusive-IP nft-drop) (ref #633) 2026-06-17 12:49:21 +02:00
8323db2690 chore(toolbox): changelog for Anti-Track v2 Plan 2a (ref #633) 2026-06-17 12:24:14 +02:00
b3e842838f fix(toolbox): autolearn must not zero pure-trackers.txt when learn import fails (ref #633) 2026-06-17 12:23:10 +02:00
2235c63986 feat(toolbox): autolearn writes learned-trackers (incl. cookie-xsite) + pure-trackers (ref #633) 2026-06-17 12:20:15 +02:00
61224031e2 chore(toolbox): log DB errors in learn signals for offline-job debuggability (ref #633) 2026-06-17 12:17:59 +02:00
c0e3ff5858 feat(toolbox): pure-trackers promotion (curated seed + conservative auto-promote) (ref #633) 2026-06-17 12:15:01 +02:00
c078f66d8f feat(toolbox): autolearn cookie-xsite signal (top-N capped) (ref #633) 2026-06-17 12:11:06 +02:00
01715363aa docs: Anti-Track v2 Plan 2a implementation plan (ref #633) 2026-06-17 12:08:03 +02:00
76cc96eff5 docs: Anti-Track v2 Plan 2a learning design (cookie-xsite + pure-promote) (ref #633) 2026-06-17 12:04:41 +02:00
4caf79be55 feat(toolbox): register privacy_guard on R3 mitm-wg fanout workers (ref #633) 2026-06-17 11:52:42 +02:00
a560f626d0 chore(toolbox): remove stale unused debian/secubox-toolbox-mitm.service (drift hazard; authoritative unit is systemd/) (ref #633) 2026-06-17 11:47:59 +02:00
11d589c19b feat(toolbox): provision privacy-jar.key + register privacy_guard addon (ref #633) 2026-06-17 11:43:50 +02:00
b71a815cc4 refactor(toolbox): protective_mode delegates tracker detection to privacy brain (ref #633) 2026-06-17 11:37:22 +02:00
c89c632caa fix(toolbox): privacy_guard — drop duplicate hook, fail-private cookie drop, doc referer fallback (ref #633) 2026-06-17 11:35:30 +02:00
f5fcc2f9aa feat(toolbox): privacy_guard hot-path addon (block/poison/anonymize) (ref #633) 2026-06-17 11:29:30 +02:00
110f060446 feat(toolbox): Anti-Track filter toggles (privacy_*/fortknox), ship dark (ref #633) 2026-06-17 11:24:26 +02:00
47773a63ab harden(toolbox): same_site empty-host guard + fail-safe tests (ref #633) 2026-06-17 11:23:00 +02:00
9315a01a4f feat(toolbox): privacy brain — layered verdict + Fort-Knox (ref #633) 2026-06-17 11:20:59 +02:00
931c936d9b style(toolbox): drop dead _ga condition, document best-effort shaping (ref #633) 2026-06-17 11:19:38 +02:00
0b1d139e40 feat(toolbox): privacy brain — deterministic fake-identity jar (ref #633) 2026-06-17 11:17:01 +02:00
83d3f136a3 fix(toolbox): drop dead imports, add learned-list test, clarify _TRACKER provenance (ref #633) 2026-06-17 11:15:13 +02:00
e2dcce4e9f feat(toolbox): privacy brain — registrable + tracker classification (ref #633) 2026-06-17 10:59:56 +02:00
2e2ab3995f docs: Anti-Track v2 core implementation plan (ref #633) 2026-06-17 10:57:26 +02:00
CyberMind
c9397e3008
Merge pull request #631 from CyberMind-FR/feature/630-make-live-ops-fixes-permanent-package-di
Make live ops fixes permanent: core traversal fix + dirs-guard timer + toolbox stream_inject default (closes #630)
2026-06-17 10:00:37 +02:00
CyberMind
206157047e
Merge pull request #629 from CyberMind-FR/feature/628-hub-health-monitor-page-vital-common-ser
hub: Health Monitor page (vital + common services, live) (closes #628)
2026-06-17 10:00:26 +02:00
CyberMind
bed4c1c6d3
Merge pull request #627 from CyberMind-FR/feature/626-haproxy-smart-self-healing-error-pages-5
haproxy: smart self-healing error pages + wire errorfile in generator (closes #626)
2026-06-17 10:00:16 +02:00
CyberMind
9ba49e3bf7
Merge pull request #625 from CyberMind-FR/feature/624-waf-robustness-package-self-healing-insp
WAF robustness: package self-healing inspector watchdog + HAProxy redispatch (durable)
2026-06-17 10:00:04 +02:00
ebf714f123 fix(core): stop clobbering /var/lib+/usr/share/secubox to 0750 + ship secubox-dirs-guard timer; toolbox: stream_inject default on (closes #630) 2026-06-17 09:33:26 +02:00
5763aa3a73 fix(hub): health monitor reads nested health-batch .modules (ref #628) 2026-06-17 08:59:40 +02:00
2a8c1b33de feat(hub): Health Monitor page — vital + common service status, live (closes #628) 2026-06-17 08:55:44 +02:00
9d1b0abade docs: spec for HAProxy complete dynamic vhost auto-discovery (landed for later) 2026-06-17 08:49:29 +02:00
41d78ef455 docs(haproxy): tidy 1.3.1 changelog (secubox-errors path, drift guard, traversal fix) 2026-06-17 08:47:11 +02:00
fbd474b2c3 fix(haproxy): postinst set shared /run|/var/lib/secubox to 0750, breaking traversal (kbin/toolbox 500) -> 0755 parents, 0750 leaves (ref #626) 2026-06-17 08:46:26 +02:00
c47e454532 fix(haproxy): ship error pages to /etc/haproxy/secubox-errors (avoid file conflict with haproxy pkg) (ref #626) 2026-06-17 08:44:06 +02:00
e12790efbd fix(haproxy): repair broken generate (set -e abort + dup backend) + drift guard (ref #626)
haproxyctl generate exited 1 producing no backends: set -e + && {} vhost-loop
chains aborted on the first non-SSL vhost, and a duplicate mitmproxy_inspector
(auto + user TOML) was fatal. Converted chains to if/then/fi, dedup user
backends. Added a drift guard: refuse to install a cfg with fewer vhosts/
backends than live, so a successful regen can't silently drop hand-maintained
vhosts (kbin/gitea/matrix/...) absent from haproxy.toml.
2026-06-17 08:36:27 +02:00
ce636273a6 feat(haproxy): smart self-healing error pages + wire errorfile in generator (closes #626)
502/503/504 poll the URL and auto-reload on backend recovery (live status +
manual retry); 400/403/408/500 branded static. haproxyctl now emits errorfile
directives (durable across regen) + retries/redispatch in defaults. Pages shipped
to /etc/haproxy/errors/.
2026-06-17 07:46:43 +02:00
4dd87eae2f fix(mitmproxy): ExecStartPost chmod raced socket creation -> wait+non-fatal (ref #624) 2026-06-17 07:36:33 +02:00
af02a9731c fix(mitmproxy): service used absent /usr/bin/uvicorn (203/EXEC crash-loop) -> python3 -m uvicorn + stale-socket unlink (ref #624) 2026-06-17 07:34:01 +02:00
663715af0f feat(mitmproxy): package self-healing WAF inspector watchdog (closes #624)
secubox-waf-watchdog timer checks inspector :8080 every 60s and auto-recovers
the mitmproxy LXC after 3 consecutive failures (rate-limited once/10min) — an
inspector crash becomes a ~3min auto-recovery instead of a multi-hour 503.
Shipped in secubox-mitmproxy; enabled in postinst, disabled in prerm. Makes the
live hotfix from the #624 incident durable across reflash.
2026-06-17 07:31:29 +02:00
CyberMind
05d6135e53
Merge pull request #622 from CyberMind-FR/fix/619-hub-dashboard-services-cache-never-warms
Some checks are pending
License Headers / check (push) Waiting to run
hub: dashboard/services cache never warms under aggregator → blocking systemctl stalls shared event loop (504s, empty widgets)
2026-06-17 07:08:16 +02:00
CyberMind
0c57960fd3
Merge pull request #621 from CyberMind-FR/feature/620-toolbox-ttfb-perf-stream-inject-async-pe
toolbox: TTFB perf — stream-inject + async per-host decision bundle (replace full-body HTML buffering)
2026-06-17 07:08:13 +02:00
CyberMind
7c3edd134c
Merge pull request #618 from CyberMind-FR/feature/617-security-posture-full-rewrite-honest-boa
security-posture: full rewrite — honest, board-truthful posture scorecard (v2)
2026-06-17 07:08:10 +02:00
aa3a9f5443 fix(toolbox): postinst must not clobber shared /var/lib|/var/cache/secubox parent modes (ref #620)
install -d -m 0750 /var/lib/secubox/toolbox re-moded the shared parent to 0750,
breaking traversal for other secubox-* daemons (kbin -> 500). Create leaves via
mkdir + explicit chown/chmod; re-assert parents 0755. Same #511 traversal class.
2026-06-16 22:31:22 +02:00
a0748c2d43 perf(toolbox): TTFB phase 2 — streaming loader inject behind stream_inject (ref #620)
request() strips Accept-Encoding for HTML navigations; responseheaders() sets
flow.response.stream to inject the /__toolbox/loader.js script into the first
chunk (no full-body buffer); response() skips the buffer path when streamed.
Compressed responses fall back to the legacy buffer path. Gated on stream_inject
(default OFF → behaviour identical to before). Loader renders banner + per-page
stats client-side.
2026-06-16 22:26:56 +02:00
bb24a7d167 fix(hub): warm services cache under aggregator + offload blocking systemctl off the event loop (closes #619)
Mounted sub-apps don't get startup/lifespan under the aggregator, so the hub
services cache never warmed and _svc() fell back to blocking per-module
systemctl on the shared loop -> /dashboard 12s timeout -> board-wide 504s +
empty hardware widget. Lazy-start the refresh on first request; offload the
systemctl/psutil work to threads in the refresh loop and in dashboard/status/
modules; de-block /alerts via cached _svc().
2026-06-16 22:21:24 +02:00
4007d7f8ca fix(security-posture): postinst must not clobber /var/lib/secubox parent mode (ref #617)
2.0.0 ran 'install -d -m 0750 /var/lib/secubox/security-posture' which re-moded
the shared parent to 0750 secubox:secubox, breaking traversal for other
secubox-* daemons (kbin/toolbox -> HTTP 500). Keep parent 0755; restrict only
the leaf; service creates its dir at runtime. Regression of the #511 class.
2026-06-16 21:58:46 +02:00
670f115a62 perf(toolbox): TTFB phase 1 — per-host decision bundle + loader endpoints + toggle (ref #620)
Additive, inert (stream_inject default OFF; inject_banner unchanged). Adds
secubox_toolbox/bundle.py (build/get_bundle + LOADER_JS), GET /__toolbox/bundle
and /__toolbox/loader.js in api.py, and the stream_inject filter toggle. The
cosmetic banner will apply client-side from the bundle, with per-page tracker/
cookie stats derived in-browser — moving that work off the proxy critical path.
Unit tests for the builder/cache.
2026-06-16 21:50:53 +02:00
6899b18ff8 docs: toolbox TTFB stream-inject design spec (ref #620) 2026-06-16 21:46:16 +02:00
6a74f3c7d0 fix(security-posture): ship nginx route drop-in + postinst nginx reload (ref #617)
The browser API 404'd because nginx routes /api/ generically to the aggregator,
which does not serve security-posture (it runs its own uvicorn socket). Ship
/etc/nginx/secubox-routes.d/security-posture.conf routing /api/v1/security-posture/
to /run/secubox/security-posture.sock, and reload nginx in postinst.
2026-06-16 15:36:35 +02:00
e75fcd5ea6 fix(security-posture): add sidebar nav element + main wrapper so /shared/sidebar.js renders the left nav (ref #617) 2026-06-16 15:29:45 +02:00
80112f5125 fix(security-posture): route collectors via aggregator socket; clean restart (ref #617)
Most sibling modules are mounted in-process by secubox-aggregator (no standalone
/run/secubox/<name>.sock); reach their public routes via aggregator.sock under
/api/v1/<name>/. socket_get now tries the direct socket then falls back to the
aggregator. Service: ExecStartPre unlinks the stale socket (fixes Address-already
-in-use on upgrade) and runs a single worker (one refresh loop / cache writer).
2026-06-16 14:48:10 +02:00
9adaa75190 feat(security-posture): full rewrite — honest posture scorecard v2 (ref #617)
Replaces the broken v1 (NameError /overview, ~95% stub TPN, hardcoded
DEFCON/performance values, zip/gather CSPN bug) with a focused engine where
every indicator carries provenance and unmeasurable signals render UNKNOWN/
MANUAL instead of faking a pass.

Backend (api/posture/): pure scoring (DEFCON dial, unit-tested), real
collectors over public sibling sockets + CrowdSec Prometheus + /proc, repaired
CSPN backbone, real TPN media overlay. Thin api/main.py (lifespan + 60s cache).
Frontend: shared hybrid-skin dark dashboard (gauge, 6 domain cards, findings,
CSPN/TPN tables). Unit tests for scoring, CSPN parsers, collectors. Sidebar
menu.d entry folds #616. Packaging: recursive install, uvicorn dep, 2.0.0.
2026-06-16 14:41:55 +02:00
3e487adf2c docs: security-posture v2 rewrite design spec (ref #617) 2026-06-16 14:25:57 +02:00
5aef304ed7 feat(monitoring): integrate Security Posture into monitoring page
Some checks are pending
License Headers / check (push) Waiting to run
- Add Security Posture API fetch function
- Add DEFCON color mapping
- Display DEFCON level in header chips
- Add Security Posture section with 4 cards:
  - DEFCON Level with color-coded border
  - CSPN Compliance score
  - TPN Compliance score
  - Performance score
- Add live update for all Security Posture metrics
- Add CSS styles for Security Posture cards (light/dark mode)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 12:30:48 +02:00
0b71a0e8b4 fix(security-posture): change service type from notify to simple
- Type=notify requires the service to send a readiness notification
- Uvicorn doesn't send NOTIFY_SOCKET by default
- Type=simple is more appropriate for this service

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 12:19:38 +02:00
03df656d36 fix(security-posture): use python3 -m uvicorn for portability
- Change ExecStart from /usr/bin/uvicorn to /usr/bin/python3 -m uvicorn
- This is more portable and works regardless of where uvicorn is installed

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 12:15:38 +02:00
4117458a70 fix(security-posture): install Python module to dist-packages
- Change installation path from /usr/lib/secubox/ to /usr/lib/python3/dist-packages/
- Use underscores in module name (secubox_security_posture) to match Python naming
- Add __init__.py for the module directory
- This ensures the Python module is importable by uvicorn

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 12:14:41 +02:00
5ba90bebf3 fix(security-posture): rename .sock to .socket for dh_installsystemd
- Remove manual systemd file installation from rules (handled by dh_installsystemd)
- Rename secubox-security-posture.sock to secubox-security-posture.socket
  so dh_installsystemd picks it up correctly
- This prevents duplicate files in /lib/systemd and /usr/lib/systemd

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 12:13:28 +02:00
62b0545343 fix(security-posture): update build dependencies and remove inter-package deps
- Add debhelper-compat (= 13) to Build-Depends
- Remove secubox-common, secubox-threat-analyst, secubox-health-doctor dependencies
- Make debian/rules executable

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 12:10:08 +02:00
d282c571bf fix(metrics): add CPU metrics to top navbar widget
- Add CPU usage calculation from /proc/stat in build_overview()
- Include cpu_pct in overview response
- Add /api/v1/metrics/summary endpoint for sidebar.js compatibility
- Maps overview data (cpu_pct, mem_pct, load) to expected format (cpu, mem, load)

Fixes top navigation bar widget not showing CPU metrics on /metrics/ page.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 09:13:33 +02:00
1e8a41c33b ui(ad-guard): limit top tracker domains to latest 5 entries
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 09:00:20 +02:00
afde96111a feat(security-posture): Add WebUI integration and fix async issues
- Add dashboard.html for standalone dashboard
- Add security-posture.js for hub integration with multiple widget types
- Add __init__.py for package imports
- Fix async/await issues in defcon.py get_summary and get_defcon_info methods
- Fix KeyError in CSPN and TPN compliance summary calculations
- Update service file to use unix socket (uds) instead of HTTP
- Fix syntax error in performance.py get_score method

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-16 08:58:43 +02:00
CyberMind
f5c7f6f6b5
Merge pull request #614 from CyberMind-FR/feature/613-threat-analyst-add-country-flags-to-top
ui(threat-analyst): country flags in Top source countries
2026-06-16 07:55:51 +02:00
d889d32714 ui(threat-analyst): country flags in Top source countries (closes #613)
flagEmoji() maps ISO alpha-2 -> regional-indicator emoji; renderTop gains a
withFlag arg used for the countries list.
2026-06-16 07:55:11 +02:00
CyberMind
4a8ee46c2d
Merge pull request #612 from CyberMind-FR/feature/611-threat-analyst-limit-recent-attacks-tabl
Some checks are pending
License Headers / check (push) Waiting to run
ui(threat-analyst): limit Recent attacks table to 5 rows
2026-06-16 07:39:39 +02:00
2c615889cf ui(threat-analyst): limit Recent attacks table to 5 rows (closes #611)
Match the Top-N lists. Stats/leaderboards unaffected (renderTopFromAlerts
runs on the full array before the table slice).
2026-06-16 07:38:56 +02:00
da0c5008df docs: gitea mis-route fix + robust WAF route propagation (#609)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-15 18:27:27 +02:00
CyberMind
994b48f39d
Merge pull request #610 from CyberMind-FR/feature/609-waf-robust-route-propagation-dir-bind-mo
fix(waf): robust route propagation — dir bind-mount + live-reload
2026-06-15 18:26:18 +02:00
e13cf925f1 fix(waf): robust route propagation — dir bind-mount + addon live-reload (closes #609)
The #603 FILE bind-mount of haproxy-routes.json binds an inode; route tools
edit via jq>tmp&&mv (new inode) so changes went stale until a container
restart (surfaced fixing git.maegia.tv mis-route). Now: wafctl uses a
DIRECTORY bind-mount (host /srv/mitmproxy -> /var/lib/secubox-waf-routes ro)
+ symlink, and the addon (both synced copies) live-reloads haproxy-routes.json
on mtime change (throttled 10s) in requestheaders -> route edits apply with
NO restart. Verified live: jq+mv add -> live-reloaded 256 routes, 0 restart.
2026-06-15 18:25:56 +02:00
6dba5a08d6 docs: WAF open-proxy fix + behind-WAF media cache (#605, #607) 2026-06-15 18:10:49 +02:00
CyberMind
211cff09b5
Merge pull request #608 from CyberMind-FR/feature/607-waf-behind-waf-media-cache-image-video-s
feat(waf): behind-WAF media cache (image/video/static) for hosted vhosts
2026-06-15 18:09:44 +02:00
3290f3b7c0 feat(waf): behind-WAF media cache for hosted vhosts (closes #607)
New media_cache.py addon (both synced copies) caches cacheable GET
media/static (image/video/audio/font/css/js) from our vhosts on disk
(URL key, 16MB/obj, 2GB LRU, TTL from max-age) and serves repeats from
cache. NOT a bypass: requests still pass secubox_waf inspection; only the
response body is served from a WAF-populated cache. Loaded in the LXC
mitmproxy.service; wafctl creates the cache dir. Toggle via
/data/mitmproxy/media-cache.json (default on). Verified live: HIT.
2026-06-15 18:09:25 +02:00
CyberMind
72b7eca12e
Merge pull request #606 from CyberMind-FR/feature/605-waf-refuse-unmapped-hosts-close-open-for
fix(waf): refuse unmapped hosts — close open forward-proxy (loops + 72% errors)
2026-06-15 17:19:25 +02:00
0d1c49307e fix(waf): refuse unmapped hosts — close open forward-proxy (closes #605)
In --mode regular the addon relayed any Host; HAProxy default_backend made
the WAF an open forward proxy abused by scanners (~72% error churn + 11
loop-508s/hr). requestheaders now serves ONLY our vhosts (routes / our
domains via routes-derived local_suffixes -> nginx 9080 / SELF_HOSTS) and
returns 421 otherwise with no upstream connect. Applied to both synced
secubox_waf.py copies. Verified live: 0 external server-connects, 0 loops,
apt/admin/kbin 200, scanners 421.
2026-06-15 17:19:06 +02:00
CyberMind
8bb546c689
Merge pull request #604 from CyberMind-FR/feature/603-waf-port-live-mitmproxy-fixes-to-source
fix(waf): port live mitmproxy fixes to source — mitmproxy-11 routing + routes bind-mount
2026-06-15 16:47:18 +02:00
05d6f97b44 fix(waf): port live mitmproxy fixes to source — mitmproxy-11 routing + routes bind-mount (closes #603)
- requestheaders hook in both synced secubox_waf.py copies: mitmproxy 11
  opens the upstream connection before the request hook, so the in-request
  redirect was too late and routed vhosts hit their public IP. Set
  flow.server_conn.address in requestheaders instead.
- wafctl: bind-mount host /srv/mitmproxy/haproxy-routes.json into the LXC at
  the addon's read path /data/mitmproxy/haproxy-routes.json (they had drifted
  → routes_count 0 → no routing); ensure the host file exists at provision.
- mitmproxy.service: warn any ExecStart drop-in must keep --set confdir
  (a dropped confdir crash-looped the WAF via ProtectHome).

Ported from the live gk2 fixes (HISTORY 2026-06-15).
2026-06-15 16:47:00 +02:00
28a73c8477 docs: WAF mitmproxy restored (confdir + mitmproxy-11 routing + routes bind-mount) 2026-06-15 16:39:01 +02:00
c64a666fa5 fix(build): build-packages.sh passes -d; docs: apt repo published + signed
Some checks are pending
License Headers / check (push) Waiting to run
build-packages.sh omitted -d, so dpkg-checkbuilddeps silently dropped
arch:all packages (incl. secubox-core) from the repo. All 144 packages now
build + are published to apt.secubox.in, signed with apt@secubox.in.
2026-06-15 15:14:55 +02:00
5325cddade docs: HISTORY for vm /vm/ container-listing fix (#601, PR #602) 2026-06-15 14:51:30 +02:00
CyberMind
1d7ca0cd22
Merge pull request #602 from CyberMind-FR/feature/601-vm-vm-shows-0-containers-lxc-enumeration
fix(vm): /vm/ shows 0 containers — sudo LXC enumeration + RAM column key
2026-06-15 14:50:46 +02:00
a610ffd276 fix(vm): lxc-ls column key RAM not MEMORY (#601)
lxc-ls -F MEMORY is rejected ('Invalid key') and emits no container rows,
so even with the sudo fix the list was empty. Use RAM (the valid key).
2026-06-15 14:49:19 +02:00
5800ad713d fix(vm): LXC enumeration via sudo so /vm/ shows real containers (closes #601)
The aggregator mounts the vm module in-process as the unprivileged secubox
user, so bare lxc-ls couldn't see root's /var/lib/lxc → 0 containers. Route
LXC read+lifecycle through sudo -n with a new read-only-ish grant
(/etc/sudoers.d/secubox-vm: lxc-ls/info/start/stop, visudo-validated);
lxc-create/destroy stay root-only. postinst reloads secubox-aggregator so
the in-process module refreshes. KVM/libvirt readings were already correct.
2026-06-15 14:46:00 +02:00
4fd0d864ee docs: HISTORY for threat-analyst CrowdSec ingestion (#599, PR #600) 2026-06-15 13:06:15 +02:00
CyberMind
f99f071642
Merge pull request #600 from CyberMind-FR/feature/599-threat-analyst-ingest-real-crowdsec-aler
feat(threat-analyst): ingest real CrowdSec alerts (read-only sudo) + auto-collect + dedup
2026-06-15 11:17:25 +02:00
77b6f2624d fix(threat-analyst): NoNewPrivileges=no so read-only sudo cscli ingestion works (#599)
NoNewPrivileges=yes blocked sudo escalation ('no new privileges flag is
set'). Match the sibling secubox-crowdsec/secubox-waf units which set
NoNewPrivileges=no for the same read-only cscli access.
2026-06-15 11:15:57 +02:00
d9fea2f9b4 feat(threat-analyst): ingest real CrowdSec alerts via read-only sudo + auto-collect + dedup (closes #599)
collect_crowdsec_alerts() shelled out to bare cscli, which fails for the
unprivileged secubox user → alerts DB empty → headline stats and Top-N
leaderboards all 0. Now goes through a backend-only read-only sudo grant
(/etc/sudoers.d/secubox-threat-analyst: cscli alerts/decisions list,
visudo-validated in postinst) fetching -l 200. Adds a 5-min backend
auto-collect loop, correct severity mapping, dedup-by-id in
get_recent_alerts, and bounded 48h compaction of alerts.jsonl (the
append-on-every-poll log was inflating counts). Frontend stays value-only.
2026-06-15 11:14:53 +02:00
65a1a8e494 docs: HISTORY for threat-analyst global overview (#597, PR #598) 2026-06-15 11:07:19 +02:00
CyberMind
8fc5dba929
Merge pull request #598 from CyberMind-FR/feature/597-threat-analyst-global-security-overview
feat(threat-analyst): global security overview from WAF + CrowdSec + firewall
2026-06-15 11:06:29 +02:00
919a88c6be fix(threat-analyst): source CrowdSec+firewall from Prometheus :6060, not root cscli/nft (#597)
The daemon runs as the unprivileged 'secubox' user, so cscli (reads
/etc/crowdsec/local_api_credentials.yaml) and 'nft list' (needs root)
both failed silently → CrowdSec showed running=false and firewall=0.
Parse CrowdSec's privilege-free Prometheus endpoint instead: cs_alerts
(detection) + cs_active_decisions (enforcement, materialized in nft by
crowdsec-firewall-bouncer). No privilege escalation, no broken-dep
(secubox-blacklist-sync #521) coupling. WebUI relabelled detection vs
enforcement.
2026-06-15 11:02:06 +02:00
60eeb79185 feat(threat-analyst): global security overview from WAF + CrowdSec + firewall (closes #597)
Add cached /overview endpoint aggregating live WAF (/run/secubox/waf.sock
/stats), CrowdSec (cscli -o json decisions+alerts) and firewall (nft -j
list set blacklist_v4/v6 element counts). Double-buffer background refresh
(60s → overview.json). WebUI gains a 'Vue globale sécurité' card row wired
via loadOverview() in loadAll(); source health line shows WAF/CrowdSec
status + last refresh age.
2026-06-15 10:56:55 +02:00
CyberMind
91cba0bbda
Merge pull request #596 from CyberMind-FR/fix/595-threat-analyst-service-ends-up-disabled
fix(threat-analyst): build-safe service enable (#595)
2026-06-15 10:50:08 +02:00
eb7fdb01e0 fix(threat-analyst): build-safe service enable in postinst (closes #595)
A plain systemctl enable no-ops during offline/image-build installs, so the
unit ended up disabled on gk2 → /threat-analyst/ page up but backend down.
Now deb-systemd-helper enable (persists to first boot) + guarded start.
secubox-threat-analyst 1.4.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:49:19 +02:00
CyberMind
2c6e6f2b51
Merge pull request #594 from CyberMind-FR/fix/593-webui-fix-metrics-mitm-0-wrong-unit-thre
fix(webui): metrics mitm=0 + threats shows IPs (#593)
2026-06-15 10:23:27 +02:00
ca1a2ede1d fix(toolbox): WebUI metrics mitm=0 (wrong unit) + threats IPs (closes #593)
- admin metrics journalctl glob secubox-toolbox-mitm* (matches live R3
  workers, not the dead R2 unit); connections from 'server connect'.
- social-aggregate by_tracker_domain folds to registrable domain + drops
  IP literals (cabine's own WAN IP was the top 'tracker'). secubox-toolbox 2.6.37.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:22:58 +02:00
e1f22b6dda docs(cspn): draft CSPN test matrix (criteria → runnable tests)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:14:40 +02:00
7a56b8de35 docs: Mistral.ai handover prompt (reprise code + analyse projet)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:37:48 +02:00
4922aada7f docs: HISTORY + WIP for the 2026-06-14 toolbox sprint (2.6.23→2.6.36)
Some checks are pending
License Headers / check (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:42:32 +02:00
CyberMind
2523333fc8
Merge pull request #591 from CyberMind-FR/fix/autolearn-exclude-antibot
fix(autolearn): exclude anti-bot from auto-block (#589 follow-up)
2026-06-14 17:39:06 +02:00
8b22d0ff62 fix(autolearn): exclude anti-bot vendors from auto-block (#589 follow-up)
Anti-bot WADs (Datadome/PerimeterX) are often the visited site's own in-path
WAF — auto-blocking them breaks the page. Learner now feeds only
operator-grade/data-broker classified trackers + threat-intel domains;
cross-site threshold 4->2. secubox-toolbox 2.6.36.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:38:44 +02:00
CyberMind
f5a4c73248
Merge pull request #590 from CyberMind-FR/feature/589-autolearn-bad-trackers-actors-from-threa
Autolearn bad trackers/actors → ad_ghost block set (#589)
2026-06-14 17:34:39 +02:00
d8a75487ac feat(toolbox): autolearn bad trackers/actors → ad_ghost block set (closes #589)
sbin/secubox-toolbox-autolearn (+ hourly timer) builds a high-confidence
learned-trackers.txt from threat-intel domain IOCs + cross-site domains
classified anti-bot/operator-grade (>=4 sites). Conservative (no plain
CDNs). ad_ghost loads it (mtime-cached), 204s learned hosts too, gated by
the autolearn filter (default on). postinst enables timer + first run.
secubox-toolbox 2.6.35. Unit-tested (learner criteria + learned block).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:34:18 +02:00
CyberMind
630cb81e03
Merge pull request #588 from CyberMind-FR/feature/587-eye-graph-domain-suffix-country-cloud-nu
Cartographie: domain-nugget cloud view (#587)
2026-06-14 17:21:02 +02:00
0f3f57e7bb feat(toolbox): domain-nugget cloud view, grouped by country (closes #587)
New '🏷️ Domaines' toggle: trackers folded to eTLD+1, packed as cloud-nugget
bubbles grouped by country (country→domain d3.pack), sized by hits,
tier-coloured, flag+name, click→domain summary. IPs hidden. Mirrors the
donut pack. secubox-toolbox 2.6.34.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:20:40 +02:00
CyberMind
7c14bdfe5e
Merge pull request #586 from CyberMind-FR/feature/578-banner-shareable-top-1-pin-quick-button
Shared broadcast pin in every banner (#578)
2026-06-14 16:44:16 +02:00
062131608f feat(toolbox): shared broadcast pin in every banner (closes #578)
Operator-set 📌 pin (or top-1 tracker) in /run/secubox/pin.json shown as
the first chip in every R2/R3 banner (24h). api: GET/POST /admin/pin +
/admin/pin/ui setter (auto-fill from top tracker). inject_banner renders it.
secubox-toolbox 2.6.33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:43:56 +02:00
CyberMind
1de8b29865
Merge pull request #585 from CyberMind-FR/feature/577-perf-video-photo-cdn-proxy-cache-shared
Shared media proxy-cache (default off) (#577)
2026-06-14 15:14:35 +02:00
3b8daf964e feat(toolbox): shared media proxy-cache, default off (closes #577)
mitmproxy_addons/media_cache.py — one upstream fetch serves all R2/R3
clients: cacheable GET media/static (image/video-segment/audio/font/css/js)
on disk (/var/cache/secubox/toolbox/media), keyed by URL. Safety: 16MB/obj
cap gated on Content-Length (large video passthrough, no RAM hold), 2GB LRU,
skips Range/auth/Set-Cookie/no-store, fail-open. Opt-in filter media_cache.
api /admin/cache stats + WebUI toggle + launcher + postinst dir.
secubox-toolbox 2.6.32. Unit-tested (HIT across clients, cap, range, segment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:13:59 +02:00
CyberMind
c043c5fca8
Merge pull request #584 from CyberMind-FR/fix/ad-ghost-collapse-placeholders
Some checks are pending
License Headers / check (push) Waiting to run
ad_ghost: remove ad placeholders entirely (collapse)
2026-06-14 15:02:02 +02:00
10d22f05b7 feat(ad_ghost): remove ad placeholders entirely — collapse, no black-hole
Reverses #576: ghosted ad slots collapse with display:none (space gone),
no void/black-hole. Host-blocking (204) still saves bandwidth.
secubox-toolbox 2.6.31.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:01:35 +02:00
CyberMind
5ece3f1208
Merge pull request #583 from CyberMind-FR/feature/576-ad-ghost-replace-ghosted-ad-with-a-css-l
ad_ghost: black-hole placeholder for ghosted ads (#576)
2026-06-14 14:58:35 +02:00
5ca4ecf455 feat(ad_ghost): ghosted ads become a CSS black-hole placeholder (closes #576)
Replace display:none with a layered dark void — radial-gradient bg + inset
glow + glowing accretion-disc (::after), content hidden. Intentional
reclaimed space instead of a collapsed gap. R3+/R4, ad_ghost filter.
secubox-toolbox 2.6.30.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:58:10 +02:00
CyberMind
ace77976fa
Merge pull request #582 from CyberMind-FR/fix/581-postinst-portal-stays-dead-after-dpkg-up
fix(postinst): portal reliably restarts after dpkg upgrade (#581)
2026-06-14 14:52:06 +02:00
4ffd66bb2d fix(toolbox): postinst reliably restarts portal after upgrade (closes #581)
The dpkg upgrade SIGTERMs the units before postinst runs, so the
try-restart loop was a no-op on the already-stopped portal -> kbin 503
(hit twice 2026-06-14). Now enabled units get a full restart (start even
if stopped); others keep try-restart. secubox-toolbox 2.6.29.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:51:42 +02:00
CyberMind
9b8144073f
Merge pull request #580 from CyberMind-FR/fix/575-eye-graph-remove-hide-all-ip-nodes-count
Eye graph: hide IP nodes + clients list top-5 (#575)
2026-06-14 14:42:57 +02:00
2f04b0fa84 fix(toolbox): eye graph hides IP nodes + clients list capped to 5 (closes #575)
- social.js: drop IP-only tracker nodes (v4/v6) from the eye force-graph —
  no more domain+IP double bubbles; remaining nodes labelled country flag +
  domain name.
- www/toolbox index.html #clients: show top 5 only (+N more note).
secubox-toolbox 2.6.28.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:42:07 +02:00
CyberMind
270f655d3a
Merge pull request #579 from CyberMind-FR/feature/574-webext-popup-protection-stats-quick-filt
webext popup: protection stats + live filter toggles (#574)
2026-06-14 12:10:41 +02:00
b366946855 feat(webext): popup protection stats + live filter toggles (closes #574)
New Protection panel in the popup: ghost savings (blocked/Mo/pages cleaned
via /admin/ghost) + live toggles for ad_ghost / ad_ghost_block / banner /
protective(off|alert|spoof) via /admin/filters. api.js: ghost/getAdminFilters/
setAdminFilters helpers. Top-tracker list stays top-5. webext 0.1.4,
tag-pin -> webext-v0.1.4, secubox-toolbox 2.6.27.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:10:20 +02:00
CyberMind
433c5ca190
Merge pull request #573 from CyberMind-FR/feature/572-banner-colorful-emoji-chip-guirlande-fla
Banner: colourful emoji-chip guirlande (#572)
2026-06-14 11:02:14 +02:00
1a23c1f78a feat(toolbox): colourful emoji-chip guirlande banner (closes #572)
inject_banner right-side stats now render as vibrant rounded pills cycling
an 8-colour festive palette with neon box-shadow glow. Pure-ASCII inline
styling, works in CSP-strict + JS variants. secubox-toolbox 2.6.26.

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

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

View File

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

View File

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

View File

@ -1,10 +1,185 @@
# TODO — SecuBox-DEB Backlog
*Mis à jour : 2026-06-13*
*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)
---
## 🔥 P0 — Immediate (in flight)
### kbin Tor endpoint — anonymized quick-switch surfing (#683)
> Capstone du couteau suisse cyber : l'anonymat de la sortie. Spec :
> `docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md`.
> Invariants : inspection préservée, fail-closed, opt-in (défaut OFF), no DNS leak, CSPN audit.
- [ ] **Transport** — Option A dialer SOCKS5 upstream (cœur Go #662, *préféré*) vs
Option B nft mark → Tor TransPort (fallback pré-#662).
- [ ] **Profil Tor egress** — réutiliser `secubox-exposure` (bootstrap/NEWNYM), egress-only.
- [ ] **API toolbox**`POST /admin/tor/{on,off}` (WG-hash scoped) + `GET /tor/state` +
`POST /tor/newnym` + état SQLite per-client (TTL 24h).
- [ ] **UI kbin** — toggle 🧅 + badge état + flag pays de sortie + bouton « nouvelle identité ».
- [ ] **Leak-guard nft** + DNS-over-Tor (test exit IP + resolver ≠ Unbound).
- [ ] **`tls_splice` OFF en mode Tor** (#649) — sinon les flux asset fuient l'IP réelle.
- [ ] **CSPN** — audit-log chaque bascule ; soak DARK (flag présent, UI cachée) avant flip.
### ToolBox clients (`clients/`)
- [x] **#531 Android scaffold + CI** — Gradle/Compose one-tap onboarding,

View File

@ -1,5 +1,253 @@
# WIP — Work In Progress
*Mis à jour : 2026-06-13*
*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.
---
## 🔄 2026-06-19 : kbin Tor egress (#683) — ToolBoX 2.7.1, implémenté DARK
Switch + tunnel Tor quick-switch livrés sur `feature/683`, **défaut OFF / fail-closed**.
Détail dans la section "Implémenté DARK" ci-dessous + HISTORY 2026-06-19.
---
## 🔄 2026-06-19 : kbin milestone — ToolBoX 2.7.0 + chapitre Tor (plan)
Checkpoint de fin de session. Pas de changement de comportement runtime — docs +
positionnement + version + plan de la lame suivante.
- ✅ **ToolBoX 2.7.0** (middle release) — clôt la ligne 2.6.x (ad-intelligence /
Anti-Track v2 / anti-bot uTLS #662), ouvre le chapitre kbin « premier outil du
couteau suisse cyber ». kbin = perf transparente + full encrypted + poison/smog +
bandeau anti-adware + safe browsing.
- ✅ **Docs kbin** — wiki [`Kbin-Toolbox.md`](../docs/wiki/Kbin-Toolbox.md),
[`FAQ-KBIN-TOR.md`](../docs/FAQ-KBIN-TOR.md), blurb README.
- ✅ **Plan #683** — spec
[`2026-06-19-kbin-tor-anonymized-surfing-design.md`](../docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md) :
endpoint Tor quick-switch (egress sortant, fail-closed, opt-in, no DNS leak,
inspection préservée). Dépend du cœur Go #662.
### ✅ Implémenté DARK — chapitre Tor (#683, ToolBoX 2.7.1, branche feature/683)
- ✅ **Transport tranché** : *torify l'egress MITM* (owner-match nft sur l'uid
`secubox-toolbox`/mitm-wg → Tor TransPort 9040 / DNSPort 5353). Inspection
préservée. Décision USER (vs dialer SOCKS5 #662 = bloqué, vs torify client = casse
l'inspection).
- ✅ **Switch** : flags `tor_mode`/`tor_preset` (filters.json) ; API kbin-gated
`GET/POST /admin/tor/{state,on,off,newnym,check-leaks}` ; onglet 🧅 WebUI (badge,
toggle, NEWNYM, sonde fuite). `tor_ctl.py` réutilise le control-port de secubox-tor.
- ✅ **Tunnel** : `conf/nft-toolbox-tor.nft` (fail-closed kill-switch + drop v6) +
`conf/torrc-toolbox-egress.conf` + reconciler root path-triggered
(`secubox-toolbox-tor.path` surveille filters.json → portail reste
NoNewPrivileges=true). nft chargé AVANT tor (pas de fenêtre clearnet).
- ✅ 166 tests verts ; license headers OK ; changelog 2.7.1.
#### ⬜ Avant flip ON (USER)
- Soak DARK puis `tor_mode=true` via l'onglet (admin.gk2).
- Test de fuite **hors-board** : l'IP réelle de la box ne doit jamais apparaître.
- Forcer `tls_splice` (#649) OFF quand armé (sinon flux asset fuient l'IP réelle).
- **Per-client (WG-hash)** : nécessite le dialer SOCKS5 du cœur Go #662 (l'owner-match
est global). Suivi sous #662.
---
## 🔄 2026-06-17/18 : Anti-Track v2 + perf/ops sprint (gk2 live)
Tout mergé sur master + déployé sur gk2. Détail dans HISTORY 2026-06-18.
- ✅ **Anti-Track v2 (#633, PR #637)** — bloque/empoisonne/anonymise, moteur
`privacy.py` + addon `privacy_guard.py`, learning (`learn.py`), IP-drop +
unbound DNS-refuse (`ip_dns.py`/`escalate.py`), bypass-seed + #filtres badges,
#social top-5. **Tourne DARK** (`privacy_enforce` unset). Wiki `Anti-Track.md`.
- ✅ **Banner saga (#636/#639, PR #638/#640)** — mitm sert loader/bundle pour
toute origine (PeerTube fixé), CSP fallback, top-bar, 1 bannière/visite.
- ✅ **#634/#635** — reset-all clients + emojis device/flag/hosting.
- ✅ **#642 (PR #643)** — social-graph ignore les edges IP-littéraux ; KPI
"Trackers vus" = table.
- ✅ **#644 (PR #645)** — hub dashboard/health-batch servis depuis cache TTL
(health-batch 3.3 s → 8 ms) ; clients/rich enrichit 12 max. **hub 1.4.6**.
- ✅ **#646 (PR #647)** — adaptive Accept-Encoding strip : plus de pages
CSP-strict tirées décompressées via le worker R3 GIL-bound. **toolbox 2.6.53**.
- ✅ **crowdsec** réparé (403 transitoire CDN → `dpkg --configure` RC=0, audit clean).
- ✅ **#623 (PR #648, merged 9950e9ec)** — clobber systémique RÉSOLU au source.
La vraie cause : boilerplate scaffold `install -d -m 750 /var/lib/secubox` +
`/run/secubox` (parents NUS) dans ~56 postinsts — écrit `-m 750` (3 chiffres),
d'où le ratage des sweeps précédents. Empiriquement prouvé que le form
`install -d -m 750 /parent/leaf` NE clobbe PAS le parent (seuls les targets
parents-nus). Fix : tous → 1777 (/run) / 0755 ; 6 lignes multi-arg splittées
(4 mettaient /var/lib en world-writable 1777) ; 3 `chmod 750 /var/log` ;
scaffold `new-package.sh` + `PATTERNS.md` ; core 1.1.8 tmpfiles.d déclare les 5
parents 0755. **PAS de mass-deploy** (60 paquets = mass-restart = risque
thundering-herd) ; live couvert par `dirs-guard.timer` ; arrive au prochain
build CI / reflash.
- ✅ **#649 Lever A — selective SNI-splice (PR #650, toolbox 2.6.54 LIVE dark)**.
New `tls_splice` addon (first in mitm-wg chain) splices pure-asset flows at the
TLS ClientHello — curated media seed (googlevideo/ytimg/fbcdn/twimg/scdn…)
autolearn-promoted never-HTML hosts — so GIL-bound R3 workers skip
forge/decrypt/parse/16-addons on no-L7-value flows. Ships `tls_splice=observe`
(DARK: classify+log, still MITM). Deployed gk2, addon loads clean, 0 runtime
errors. Answer to "do we need full mitm?": YES for outbound HTTPS (per-host cert
forging is intrinsic) — but only decrypt what we modify. Lever B (Go/Rust core)
= strategic follow-up. WAF = later.
### ⬜ Next Up
- **#649 SOAK → FLIP** — review `would-splice` logs + `/run/secubox/splice.json`
on real traffic for a soak window, confirm no first-party/HTML host is
classified, then flip `tls_splice=on` in `/etc/secubox/toolbox/filters.json`
(hot-reload). Before flip: the fortknox-via-WebUI refresh gap is already fixed.
- **Lever B (#649 follow-up)** — Go/Rust forging-proxy core if A isn't enough.
- **Anti-Track v2 ARMING** (décision USER, gated) — soak observe-only puis flip
`privacy_enforce=true` ; régénérer `data/cdn-allowlist.txt` depuis les plages
publiques avant `privacy_ip_drop` ; `unbound-checkconf` avant `privacy_dns_feed`.
- **Tunnel R3 perf** — l'encoding fix aide ; reste la contention CPU board-wide
(load ~5/4 cœurs, workers mono-thread). Lever suivant = réduire les co-tenants
(gitea/R2-mitm/crowdsec/metrics) ou isoler le mitm, pas du tuning d'addon.
- **#615** — Security Posture dans la navbar du Hub (petit enhancement).
- **#592 webmail-hub** — BLOQUÉ : besoin client OAuth Google + vhost ; Phase 1
IMAP (Gandi/OVH) peut démarrer sans OAuth.
---
## 🔄 2026-06-14 : ToolBoX privacy/perf sprint — 2.6.36 live (see HISTORY)
Tout mergé + déployé sur gk2 (kbin sain, `secubox-toolbox 2.6.36`).
Détail complet dans HISTORY 2026-06-14. Résumé :
- ✅ Protective spoof (#560), modular filters + ad-ghoster (#566, collapse
#584), media cache opt-in (#577), autolearn (#589/#591), DPI media donut
(#570), donut + domain-nugget cartographie (#553/#587, IP cachées #575,
favicons #555), guirlande banner + pin (#572/#578), webext popup panel
(#574), /ca/fingerprint R3 (#562), postinst restart fix (#581),
detect_antibot deployment-vs-challenge (#564).
- ✅ Clients : APK v0.3.0 (zero-tap launch+boot), webext v0.1.4.
- ✅ Fixes live : Nextcloud iPhone photos (files_antivirus off + PHP
limits), kbin 503 (#581).
### ⬜ Next Up
- **#592 secubox-webmail-hub** (Gmail OAuth2 + Gandi + OVH, inbox unifié) —
design filé, **BLOQUÉ** : besoin d'un client OAuth Google (client_id/
secret/redirect) + nom de vhost + (read-only Phase 1 ?). Phase 1 IMAP
(Gandi/OVH) peut démarrer sans OAuth sur "start phase 1".
- Côté user : re-trust R3 CA `D5:E4:3A` sur l'iPhone (bannière HTTPS) ;
tester l'upload photo Nextcloud ; activer `media_cache` si voulu
(`/admin/filters/ui`) et surveiller `/admin/cache`.
---

View File

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

View File

@ -63,6 +63,11 @@ jobs:
# Build the flat {package, arch} matrix. Honour the workflow_dispatch
# `arch` and `package` filters if set (empty on `push: tags` events).
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:-}"
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 install -y -qq \
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
# aarch64-linux-gnu-{strip,objdump} when -a arm64 is passed.
# Without these, arch-specific packages shipping prebuilt
@ -213,7 +223,18 @@ jobs:
# no-op; for arm64 jobs that don't compile native code (Python +
# prebuilt arm64 binaries — like sentinelle-gsm), -a arm64 is
# 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 }})"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,8 @@ android {
applicationId = "in.secubox.toolbox"
minSdk = 26
targetSdk = 34
versionCode = 3
versionName = "0.3.0"
versionCode = 4
versionName = "0.4.0"
}
buildTypes {

View File

@ -88,12 +88,23 @@ fun OnboardApp() {
busy = false; status = "Borne injoignable — vérifie le réseau."
} else {
step = Step.RootAuto
val onb = RootOnboard(api, ctx.cacheDir)
val onb = RootOnboard(api, ctx.cacheDir, ctx.filesDir)
val out = withContext(Dispatchers.IO) {
onb.runSilent { line -> scope.launch(Dispatchers.Main) { rootLog.add(line) } }
}
busy = false
onTunnel = out.verified
// #683 — surface kbin Tor egress status (anonymised exit) if on.
rootLog.add(withContext(Dispatchers.IO) {
val t = api.torStatus()
when {
t == null -> "• Statut Tor : indisponible"
!t.optBoolean("tor_mode", false) -> "• Mode Tor : inactif"
t.optBoolean("running", false) ->
"🧅 Mode Tor ACTIF — sortie anonymisée${t.optString("exit_ip", "").let { if (it.isNotBlank() && it != "null") " ($it)" else "" }}"
else -> "🧅 Mode Tor activé — tunnel Tor en démarrage…"
}
})
when {
out.verified -> step = Step.Done
out.wgViaApp -> { step = Step.ImportProfile

View File

@ -50,7 +50,7 @@ class OnboardService : Service() {
kotlinx.coroutines.delay(2000)
}
if (!ok) return
RootOnboard(api, cacheDir).runSilent { /* headless: no UI log */ }
RootOnboard(api, cacheDir, filesDir).runSilent { /* headless: no UI log */ }
}
private fun buildNotification(): Notification {

View File

@ -9,7 +9,14 @@ import java.io.File
import java.security.MessageDigest
import java.security.cert.CertificateFactory
class RootOnboard(private val api: ToolboxApi, private val cacheDir: File) {
class RootOnboard(
private val api: ToolboxApi,
private val cacheDir: File,
// #683: app-internal storage for the STABLE WG identity (survives reboot).
// Defaults to cacheDir so older call sites still compile, but real callers
// pass filesDir so the identity persists instead of churning each boot.
private val filesDir: File = cacheDir,
) {
/** A line appended to the on-screen log during the silent run. */
fun interface Logger { fun log(line: String) }
@ -123,8 +130,10 @@ class RootOnboard(private val api: ToolboxApi, private val cacheDir: File) {
log.log("• Noyau sans module WireGuard — bascule sur l'app WireGuard")
return false
}
log.log("• Génération du profil WireGuard…")
val conf = api.downloadProfile(cacheDir).readText()
log.log("• Profil WireGuard (identité stable)…")
// #683: reuse the persisted keypair so the device keeps ONE identity
// across reboots (no more stats reset to a fresh empty hash each boot).
val conf = api.persistentProfile(filesDir).readText()
val wg = parse(conf) ?: run { log.log("✗ profil illisible"); return false }
val iface = "wg-village3b"
val r = RootShell.runScript(

View File

@ -51,6 +51,41 @@ class ToolboxApi(rawHost: String) {
fun downloadCa(cacheDir: File): File = download("/wg/ca.crt", "village3b-ca.crt", cacheDir)
fun downloadProfile(cacheDir: File): File = download("/wg/profile/new", "village3b-toolbox.conf", cacheDir)
/**
* The device's STABLE WireGuard identity (#683 lost-referrer fix).
*
* `/wg/profile/new` mints a FRESH keypair on every call. The onboarding
* runs on every boot, so calling it each time gave the device a NEW pubkey
* new sha256(pubkey) identity hash its stats/social history reset to an
* empty bucket on every reboot/reconnect. Here we fetch a peer ONCE and
* persist the .conf in app-internal `filesDir` (survives reboots, unlike the
* evictable cacheDir). Every later call reuses the SAME keypair SAME
* identity the device keeps one continuous history.
*
* Survives reboot/reconnect/app-restart. (Reinstall still wipes filesDir;
* cross-reinstall persistence would need allowBackup kept off for CSPN.)
*/
fun persistentProfile(filesDir: File): File {
val stored = File(filesDir, "identity-wg.conf")
if (stored.exists() && stored.length() > 0L &&
stored.readText().contains("PrivateKey", ignoreCase = true)) {
return stored
}
val fresh = download("/wg/profile/new", "identity-wg.conf.tmp", filesDir)
fresh.copyTo(stored, overwrite = true)
fresh.delete()
return stored
}
/** kbin Tor egress status for the client UI (read-only, kbin-safe). */
fun torStatus(): JSONObject? {
val c = open("/wg/tor-status")
return try {
if (c.responseCode !in 200..299) null
else JSONObject(c.inputStream.bufferedReader().readText())
} catch (_: Exception) { null } finally { c.disconnect() }
}
/** R3 tunnel status. Returns (onTunnel, peerIp?). */
fun r3Check(): Pair<Boolean, String?> {
val c = open("/wg/r3-check")

View File

@ -31,7 +31,7 @@ to your cabine over the R3 tunnel — no third-party calls.
Published release `.xpi` (downloadable directly):
```
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.3/secubox-toolbox-webext.xpi
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi
```
The toolbox also serves it from the cabine:

View File

@ -65,6 +65,17 @@ async function r3Check(host) {
}
}
// #683 — kbin Tor egress status (public, kbin-safe endpoint).
async function torStatus(host) {
try {
const resp = await fetch(`${baseUrl(host)}/wg/tor-status`, { credentials: "omit" });
if (!resp.ok) return { tor_mode: false };
return await resp.json();
} catch (_) {
return { tor_mode: false };
}
}
// graph: the per-session cartographie JSON. Throws on HTTP error so the
// caller can show "token expired — re-pair".
async function graph(host, token, since) {
@ -89,6 +100,29 @@ async function wipe(host, token) {
return await resp.json();
}
// #574 — protection stats + modular filter toggles (cabine admin API).
async function ghost(host) {
try {
const r = await fetch(`${baseUrl(host)}/admin/ghost`, { credentials: "omit" });
return r.ok ? await r.json() : null;
} catch (_) { return null; }
}
async function getAdminFilters(host) {
try {
const r = await fetch(`${baseUrl(host)}/admin/filters`, { credentials: "omit" });
return r.ok ? await r.json() : null;
} catch (_) { return null; }
}
async function setAdminFilters(host, patch) {
const r = await fetch(`${baseUrl(host)}/admin/filters`, {
method: "POST", credentials: "omit",
headers: { "content-type": "application/json" },
body: JSON.stringify(patch),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return await r.json();
}
// Favicon of a major site/tracker via the cabine's server-side proxy
// (7-day cached PNG, transparent 1×1 fallback) — no third-party call.
function faviconUrl(host, domain) {
@ -110,8 +144,12 @@ const SbxApi = {
setConfig,
pair,
r3Check,
torStatus,
graph,
wipe,
ghost,
getAdminFilters,
setAdminFilters,
faviconUrl,
socialUrl,
reportUrl,

View File

@ -15,7 +15,7 @@
set -euo pipefail
DEFAULT_HOST="kbin.gk2.secubox.in"
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.3/secubox-toolbox-webext.xpi"
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.4/secubox-toolbox-webext.xpi"
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
say(){ printf '\033[1;36m▸\033[0m %s\n' "$*"; }

View File

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "SecuBox ToolBoX — Cartographie sociale",
"version": "0.1.3",
"version": "0.1.5",
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
"browser_specific_settings": {
"gecko": {

View File

@ -69,6 +69,14 @@ button.danger { color: var(--cinnabar); border-color: var(--cinnabar); }
.row .fav { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; background: #1a1a22; object-fit: contain; }
.row .dom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .hits { color: var(--muted); }
/* #574 — protection panel */
#protect { margin: 8px 0; padding: 8px; background: #0e0e15; border: 1px solid #222; border-radius: 8px; }
.phead { color: var(--matrix); font-weight: 700; font-size: 12px; margin-bottom: 6px; }
.gstat { color: var(--muted); font-weight: 400; font-size: 10px; }
.tg { display: flex; align-items: center; gap: 6px; font-size: 11px; padding: 3px 0; }
.tg select { margin-left: auto; background: #14141c; color: var(--text); border: 1px solid #333; border-radius: 4px; }
#protect input { accent-color: var(--void); }
.tier { font-size: 9px; padding: 1px 4px; border-radius: 3px; }
.tier.cdn { background: #1d2a33; color: var(--cyan); }
.tier.ab { background: #2a1416; color: var(--cinnabar); }

View File

@ -10,6 +10,7 @@
<body>
<header>
<span class="logo">👁️ VILLAGE3B</span>
<span id="tordot" class="r3 off" title="Mode Tor" style="display:none">🧅</span>
<span id="r3dot" class="r3 off" title="État du tunnel R3">R3</span>
</header>
@ -36,6 +37,17 @@
<div class="toplist" id="topList"></div>
<section id="protect">
<div class="phead">🛡 Protection <span id="ghostStat" class="gstat"></span></div>
<label class="tg"><input type="checkbox" data-f="ad_ghost"> Masquer pubs/bannières (R3+)</label>
<label class="tg"><input type="checkbox" data-f="ad_ghost_block"> Bloquer hôtes pub (économie)</label>
<label class="tg"><input type="checkbox" data-f="banner"> Bannière transparence</label>
<label class="tg">Mode protecteur
<select data-f="protective"><option value="off">off</option><option value="alert">alert</option><option value="spoof">spoof</option></select>
</label>
<p id="protectMsg" class="muted"></p>
</section>
<div class="actions">
<button id="openFull">🗺️ Cartographie complète</button>
<button id="pdf">📄 Rapport PDF</button>

View File

@ -64,6 +64,41 @@ function paint(data) {
fillTopList(data.nodes);
}
// #574 — protection stats + live filter toggles in the popup.
async function loadProtection() {
const sec = $("protect");
if (!sec) return;
const g = await api.ghost(curHost);
if (g) {
$("ghostStat").textContent =
`${g.blocked_requests || 0} bloqués · ~${g.mb_saved_est || 0} Mo · ${g.pages_cleaned || 0} nettoyées`;
}
const f = await api.getAdminFilters(curHost);
if (!f) { sec.style.opacity = "0.5"; return; }
sec.style.opacity = "1";
sec.querySelectorAll("[data-f]").forEach((el) => {
const k = el.dataset.f;
if (el.type === "checkbox") el.checked = !!f[k];
else el.value = f[k];
});
if (!sec.dataset.wired) {
sec.dataset.wired = "1";
sec.querySelectorAll("[data-f]").forEach((el) => {
el.addEventListener("change", async () => {
const v = el.type === "checkbox" ? el.checked : el.value;
try {
await api.setAdminFilters(curHost, { [el.dataset.f]: v });
$("protectMsg").textContent = "✓ appliqué";
setTimeout(() => ($("protectMsg").textContent = ""), 1000);
loadProtection();
} catch (e) {
$("protectMsg").textContent = "erreur : " + e.message;
}
});
});
}
}
async function load() {
const cfg = await api.getConfig();
curHost = cfg.host || api.DEFAULTS.host;
@ -76,6 +111,21 @@ async function load() {
dot.title = r.tunnel ? `Tunnel R3 actif (${r.peer_ip || "?"})` : "Hors tunnel R3";
});
// #683 — Tor egress indicator (only visible when kbin Tor mode is on)
api.torStatus(cfg.host).then((t) => {
const dot = $("tordot");
if (!dot) return;
if (t && t.tor_mode) {
dot.style.display = "";
dot.className = "r3 " + (t.running ? "on" : "off");
dot.title = t.running
? `Mode Tor actif — sortie anonymisée${t.exit_ip ? " (" + t.exit_ip + ")" : ""}`
: "Mode Tor activé — démarrage du tunnel…";
} else {
dot.style.display = "none";
}
});
if (!cfg.token) {
$("host").value = cfg.host;
show("pair");
@ -87,6 +137,7 @@ async function load() {
const data = await api.graph(cfg.host, cfg.token, cfg.since);
paint(data);
$("liveMsg").textContent = "";
loadProtection();
} catch (e) {
if (String(e.message) === "token-expired") {
// token died — drop it and go back to pairing

View File

@ -55,4 +55,13 @@ server {
proxy_pass http://unix:/run/secubox/system.sock:/;
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;
}

107
docs/AI-HANDOVER-mistral.md Normal file
View File

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

93
docs/FAQ-KBIN-TOR.md Normal file
View File

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

View File

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

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

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