Commit Graph

190 Commits

Author SHA1 Message Date
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
bd54d82ae2 docs: anti-tracking v2 design — layered block/poison/anonymize 2026-06-17 15:15:28 +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
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
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
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
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
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
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
2e2ab3995f docs: Anti-Track v2 core implementation plan (ref #633) 2026-06-17 10:57:26 +02:00
9d1b0abade docs: spec for HAProxy complete dynamic vhost auto-discovery (landed for later) 2026-06-17 08:49:29 +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
6899b18ff8 docs: toolbox TTFB stream-inject design spec (ref #620) 2026-06-16 21:46:16 +02:00
3e487adf2c docs: security-posture v2 rewrite design spec (ref #617) 2026-06-16 14:25:57 +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)
Some checks are pending
License Headers / check (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:37:48 +02:00
2b1d2173c0 docs: add Cartographie sociale 'You Have Been Tracked' README section + poster brief (ref #502)
Some checks are pending
License Headers / check (push) Waiting to run
- README: new VILLAGE3B subsection for the Phase 11 social mapping
    feature (per-client /social/me, d3 graph, RGPD wipe, operator tab).
  - docs/marketing/POSTER-you-have-been-tracked.md: poster brief with
    the real kbin metrics (218 trackers, 142 sites, 234 JA4, 83693
    connexions) + doctrine guardrails + declension variants.
2026-06-10 12:52:36 +02:00
3d2ecd574f feat(secubox-toolbox): 3 QR code endpoints + splash QR display + 9th dashboard widget (device fingerprint) + README poster integration (ref #495 #497)
## QR endpoints (python-qrcode)

  GET /qr/splash.png    → QR encodes the splash URL (Cabine join)
  GET /qr/cert.png      → QR encodes /ca/mobileconfig (iOS profile)
  GET /qr/webclip.png   → QR encodes /ca/webclip-cabine.mobileconfig
  GET /qr/{target}.png  → generic : maps splash/cert/webclip/report/
                          fingerprint to fixed URLs, or encodes literal

  PNG output, Cache-Control: public, max-age=3600.
  Verified : 232x232 1-bit grayscale, 384-553 bytes each.

  debian/control adds python3-qrcode dependency.

## Splash QR card

The cert install card on the splash now shows 3 QR codes (① SPLASH,
② CERT iPHONE, ③ WEBCLIP HOME) on a white background grid below the
test button. Scannable from another device for cross-installation
(useful at the cabine : user shows QR to others without typing URLs).

## Dashboard hero : 9th widget (device fingerprint)

Mirroring the poster layout, added a full-width 9th widget below the
2 rows of 4 KPI widgets :

  ┌─────────────────────────────────────────────────────────┐
  │ 📱 📱 Empreinte device                                   │
  │      iPhone iOS 26.3 · 🟢 Chrome 148                     │
  │      21 UAs distincts observés                           │
  └─────────────────────────────────────────────────────────┘

Now matches the 9-widget metrics layout from the public poster :
🌐 connexions · 📡 hôtes ·  OK 2xx · 🔒 cert-pin
📺 apps · 🍪 trackers · 🌍 pays · 🎯 score
📱 device (full-width)

## README + assets/poster integration (#497)

  - README.md : new section at top with poster image embed + 3 doc links
  - docs/assets/poster/README.md : print specs + variants list + license
  - Placeholder docs/assets/poster/village3b-A2.png expected (user provides)

Wiki integration pending : will be added via gh wiki API once issue #497
phase 2 launches.

## Rebase

Branch was rebased onto master (which now has all PR #493 Phase 3
commits). control merged : Phase 3 fonts + Phase 5 lxc deps + new
python3-qrcode in one cohesive Depends block.
2026-06-05 11:24:03 +02:00
2efdd82bbf docs(marketing): poster grand public + dossier presse update Phase 3+5+6 (ref #492 #480)
## 2 docs marketing

### POSTER-grand-public-village3b.md (NEW)
Brief de poster A2 portrait pour affichage public (médiathèque, école,
mairie, EHPAD). 6 zones :
  1. Titre 📡 VILLAGE3B + USP (anonyme + gratuit + open source)
  2. 3 niveaux d'opt-in R0/R1/R2 visualisés
  3. Ce que contient le rapport (KPI + categories + transparency)
  4. Conformité CSPN/LCEN + 5 garanties
  5. 3 QR codes (splash, cert, webclip)
  6. Footer CyberMind + contact + soutiens

Variantes A4/A5/dépliant 3-volets documentées. Charte couleurs P31 phosphor
green + Cosmos Black + Gold Hermétique alignée avec DESIGN-CHARTER.md.

Output formats à générer : PDF print 300dpi, SVG Inkscape source, PNG
1920x1080 social, Story IG 1080x1920, LinkedIn banner.

### PROMPT-claude-presse-gouv.md (UPDATED)
Ajout d'une ANNEXE 'Mise à jour 2026-06-05' qui synchronise le dossier
presse avec l'évolution récente :

  - Modèle d'opt-in à 3 niveaux + R3 WireGuard (Phase 6)
  - Transparence radicale : 'La cabine SAIT se taire, et DIT QUAND elle
    se tait' — différenciateur fondamental
  - Architecture mitm disjoints (Phase 5 #495 séparation host/LXC)
  - Mode WireGuard portable kbin.gk2.net:51820 (Phase 6 #496) pour
    usage HORS cabine physique = inclusion territoriale + EHPAD/domicile
  - Engine sensibilité réglable (low/medium/high/paranoid) : opérateur
    collectivité adapte sans toucher code
  - Empreinte hardware MochaBin + Debian + coût ~250€ HT
  - AI-HANDOVER brief = continuité du commun numérique au-delà d'un auteur
  - 5 composants installables iPhone (cert + webclip + WG + guide)
  - Engagement personnel : 'pas de boîte noire, commun numérique'

Tous ces éléments à intégrer par le rédacteur LLM dans la tribune,
le CP, les candidatures France.gouv (ANCT/France-Services/Beta.gouv/
ANSSI/France 2030/CNIL).
2026-06-05 11:07:34 +02:00
17ff8c5cfb docs: AI-HANDOVER tech brief for external LLM (GPT-5/Gemini/Claude) (ref #492)
13-section self-contained brief :
  1. Mission produit + licence CMSD strict
  2. Stack technique exhaustif
  3. 2 mitm DISJOINTES (ToolBox host vs WAF LXC) — règle d'or
  4. Niveaux R0/R1/R2 + R3 roadmap
  5. Phases 1->8 status
  6. Endpoints API actuels + Phase 6 ajouts
  7. Rule engine sensitivity profiles
  8. Drapeaux + emojis (5 fonts embedded PDF)
  9. Quick-up composants splash
  10. Dette technique connue (#494, WAF, certs, perms)
  11. Contributing rules (worktree + licence header + pas Claude/Anthropic ref + tests E2E + CSPN)
  12. Status 2026-06-05 + roadmap actif
  13. Contact

Destiné à brief un autre assistant LLM sans contexte préalable pour
contribuer en cohérence avec l'écosystème cabine VILLAGE3B.
2026-06-05 11:05:13 +02:00
CyberMind
43b87d0111
docs: license correction — retirer mentions AGPL/Apache (ref #481) (#483)
Le projet est sous LicenseRef-CMSD-1.0 (Source-Disclosed License) EXCLUSIVEMENT.
Les mentions AGPL dans les textes user-facing étaient incorrectes — corrigé.

Fichiers modifiés :
- packages/secubox-toolbox/README.md (section Licence)
- packages/secubox-toolbox/conf/splash.html.j2 (footer)
- packages/secubox-toolbox/conf/report-live.html.j2 (footer)
- packages/secubox-toolbox/secubox_toolbox/reports.py (PDF footer)
- docs/marketing/PROMPT-claude-presse-gouv.md (7 mentions corrigées + 1 ajout
  d'avertissement explicite "NE PAS confondre avec AGPL ou Apache" pour
  guider la future session Claude)

Le prompt presse a aussi été ajouté à cette branche pour éviter la divergence
master-vs-worktree (le file était untracked dans master).

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-06-05 06:27:43 +02:00
948774f7fc docs(specs): integrate CM-WALL-EGRESS-2026-06 v0.1.0-draft + agent context
Spec stored under docs/specs/ for source of truth.

Module : WALL (paire complémentaire WALL↔MIND), compagnon de
CM-MESH-MPCIE-2026-06. Détection egress + corrélation menaces sur
Gondwana-Air.

Frontière R1/R2 intangible :
  R1 — métadonnées (flux, DNS, TLS-méta, IP-intel, NIDS) : licite
       sur accès ouvert, détection passive par défaut.
  R2 — contenu (TLS-break, DPI profonde, file-extract) : segments
       consentis/gérés UNIQUEMENT, opt-in journalisé.

Couches L0-L5 décrites avec outils ARM64 (Suricata 7.x, Zeek,
unbound+RPZ, goflow2/nfdump, feeds abuse.ch/ET/Spamhaus/MISP).

Contrainte ECH 2026 actée : SNI chiffré → poids sur L0 flux + L2
JA4 + L3 IP-intel + cadence, jamais sur SNI seul.

packages/secubox-egress/CLAUDE.md : agent context complet, DoD §1,
garde-fous §2 (R1/R2 intangible, OPAD, ECH-ready, secrets hors
TOML, LCEN), arborescence §3, TOML §4, API §5 (5 endpoints), NIDS
§6, .deb §7, ordre tâches §8, tests bench Maurienne §9.

Refus runtime câblé dans api.py : POST /wall/quarantine avec
regime=r2 + vlan=open_access_vlan → 403 (interception illicite).
Corrélation correlate.py exige multi_signal_threshold concordants
avant verdict quarantine (anti-faux-positif cloud-intel).
2026-06-02 11:13:44 +02:00
8a853ba3e8 docs(specs): integrate CM-MESH-MPCIE-2026-06 v0.2.1-draft
Spec stored under docs/specs/ for source of truth, referenced by the
two module CLAUDE.md files.

Major changes from v0.1.0:

  v0.2.0 — hardware matrix FROZEN on real inventory :
    - Backhaul WLE900VX / QCA9880 / ath10k_pci (mPCIe PCIe, 3x3)
    - Access MT7632U / mt76x2u (USB) or AR9271 / ath9k_htc (firmware-free)
    - MT7615 + ath9k mPCIe ABANDONED (not in stock)
    - Globalscale NXP card DISCARDED (no mesh point, opaque firmware)
    - BLE hole CONFIRMED (no LESC controller in stock)
    - ath10k 802.11s = validation item #1

  v0.2.1 — refinements :
    - "S305-8946-1A4C" shielded modules IDENTIFIED = MT7632U (open #6 closed)
    - Method clarified : USB enumeration over mPCIe adapter is artifact ;
      chip ID (MT7632U = USB silicon) is what determines bus
    - Multi-radio per node ACTED (1 mPCIe + 2 USB3 = up to 3 radios)
    - mt76 backhaul fallback documented if ath10k 802.11s disappoints

CLAUDE.md updates:

  - secubox-mesh : new prologue section reflecting v0.2.1 matrix figée,
    validation priority items, mt76 fallback noted. Code stays
    chipset-agnostic, default TOML now reflects retained stock.
  - secubox-mesh-bt : new prologue section flagging BLE hardware hole
    blocks S2/S3 ; Option A (source BLE 5.x mainline) vs Option B
    (ESP32 front-end, in stock) decision REQUIRED before gatt/oob/
    session implementation. Architecture impact documented.
2026-06-02 11:06:40 +02:00
CyberMind
6fad8bd99f
Merge pull request #412 from CyberMind-FR/feature/409-secubox-avatar-smart-media-cookie-hoster
secubox-avatar: Companion personas + client-encrypted session vault (#409)
2026-05-29 09:20:48 +02:00
787c6b03e8 fix(nextcloud): give LXC systemd-resolved a DNS upstream; document LXC DNS gotcha
Static resolv.conf in the container is hijacked by systemd-resolved's empty
127.0.0.53 stub, killing DNS (no appstore/updates/external mounts) while NAT
egress still works. Drop a resolved.conf.d upstream at install. Add
docs/FAQ-LXC-DNS.md describing symptom/cause/fix. Live NC already patched.
2026-05-29 07:10:39 +02:00
79d6e08ffc docs(wiki): SecuBox Companion — avatar personas, selectable picker, Become (ref #409)
Document the persona model: a named, browser-encrypted group of site sessions
the operator picks from a tick-list of their live logins, reapplied with one
Become click on any same-LAN box. Explains the <all_urls> read / ticked-write
posture and that ciphertext never leaves the SecuBox.
2026-05-28 14:15:02 +02:00
c14f5b2d23 docs(audit): mesh cluster inspection — master-link is dead code, mirror is miscategorized
Per-package inspection of the mesh cluster found:

  - secubox-mesh (Yggdrasil daemon control), secubox-meshname
    (Meshname DNS resolver), secubox-daemon (Go mesh binary) — all
    distinct, keep.
  - secubox-mirror — miscategorized: it's an APT/CDN cache, not
    mesh-related at all. Audit grouped it by prefix matching.
  - secubox-master-link — effectively dead on a running system:
    ships an 851-LOC API and frontend but no nginx config; its 21
    API endpoints (/run/secubox/master-link.sock) are unreachable
    from the web. The "master-link" UI operators actually see is
    served by secubox-p2p (which has its own /master-link/status +
    /master-link/token endpoints + ships the frontend to
    /var/www/secubox/master-link/).
  - secubox-p2p is the canonical master-link owner.

Real consolidation candidate: master-link → p2p fold. Bigger than
the mmpm/magicmirror fold (issue #381) because of asymmetric API
surface — master-link's 21 endpoints vs p2p's 2 existing
/master-link/* endpoints means the merge isn't mechanical. Defer
to a separate per-cluster issue with its own scope decisions.

Mesh cluster net reduction potential: -1 (after master-link → p2p
fold), not the -4 the original audit projected. Total realistic
floor unchanged from previous revision (~130-136 packages).
2026-05-27 08:31:36 +02:00
66adab2f5c docs(audit): Tier 3 + Tier 4 revision — pattern is naming-affinity, not redundancy
Per-package code inspection of the streamlit (already revised), dpi
(revised in 382), dns, and threats clusters all returned the same
finding: the audit's initial cluster groupings were based on naming
similarity, not architectural redundancy.

DNS cluster (5 → 5, was 5 → 1):
  Five distinct DNS-layer subsystems, no shared config files. The
  audit's "all overlap on /etc/resolv.conf and/or unbound" claim was
  wrong — grep for /etc/resolv.conf in any of them returns zero hits.
  Each touches its own config + data directory.

Threats cluster (7 → 7, was 7 → 1):
  Seven distinct security capabilities with distinct backends
  (crowdsec scenarios, nftables, dnsmasq logs, remote feeds, own
  data dirs). Only the umbrella secubox-threats actually subscribes
  to the CrowdSec/Suricata event bus the audit claimed all seven
  did.

Tier 4 table revised to "candidates for inspection" — each cluster
needs the same per-package code check before any merge work.

Realistic-reduction floor updated from ~100 packages to ~130-136
(a 4-8% reduction from 141 rather than the originally projected 28%).
Where consolidation actually paid off: Tier 0 packaging bug (#378),
Tier 1 mail dead-code (#380), Tier 2 mmpm-magicmirror scaffolding
duplicate (#381), Tier 2 dpi description clarity (#382).

The pattern observation section explicitly flags this for future
work: most "fuzzy clusters" in out/clusters-fuzzy.txt are naming
overlaps, not merge targets. Treat the audit's cluster lists as
candidates for Description-clarity passes rather than packaging
merges.
2026-05-27 08:19:05 +02:00
7e006ae57f docs(audit): revise Tier 2 dpi cluster — naming fix only, no merge (ref #382)
After investigation in #382, the dpi cluster's four packages have
distinct operator roles, not duplicated ones:
  - secubox-netifyd: daemon lifecycle dashboard
  - secubox-dpi: analytics layer on top of netifyd
  - secubox-ndpid: dedicated dashboard for the different nDPId engine
  - secubox-mediaflow: streaming/VoIP consumer of DPI

The audit's original "dual-stream netifyd/nDPId" framing was
aspirational; the code is netifyd-only and nDPId analysis already
belongs to secubox-ndpid.

Real bug was a misleading Description on secubox-dpi (fixed in #382),
not a redundant-package situation. Keep all four packages.

Updates the realistic-reduction table: Tier 2 net is -1
(magicmirror only) rather than -3.
2026-05-27 08:15:10 +02:00
ad0550a461 docs(audit): revise Tier 2 — drop streamlit/forge merge from recommendation
On closer inspection, secubox-streamlit and secubox-streamforge serve
distinct operator workflows that map to two different LuCI apps in the
OpenWrt source. Streamlit is the LXC-backed runtime that runs apps
(pulls in lxc + debootstrap + sudoers); streamforge is the
template/authoring tool (core deps only, 660-line FastAPI).

Merging would force every streamforge operator to install lxc and
debootstrap or require a complicated Recommends-vs-Depends split for
marginal benefit (net -1 package).

Also revises the realistic-reduction table: Tier 2 net is -3, not -4.
2026-05-27 07:43:10 +02:00
a85129f5c6 docs(audit): Phase 1 consolidation audit — script + findings (ref #378)
Adds scripts/audit-packages.py to scan packages/secubox-*/ and emit
structured artifacts under out/ (gitignored): packages.json,
packages.dot dependency graph, empty-packages.txt, prefix-based
clusters, and a hand-curated fuzzy-cluster report with per-member
evidence (deps, routes, services).

Findings doc 2026-05-27-secubox-consolidation-audit.md synthesises the
data into Tier 0-4 merge proposals grounded in real package contents,
not the plan stub's illustrative guesses. Key corrections vs the stub:

- 141 real packages (not "100+"); secubox-core is the substrate for 131
- streamlit-idle is already an in-package service variant, not a
  separate package — that consolidation is half-done
- meta{blogizer,bolizer,catalog,oblizer} are real distinct services,
  not typo proliferation (recommend rename for clarity, not merge)
- Two genuine packaging bugs surfaced: secubox-c3box binary name
  collision between Python dashboard and Go daemon (issue #378),
  secubox-smart-strip has no debian/ directory at all

Also imports two pre-existing 2026-05-26 plan stubs (consolidation,
build-scripts-refactor) that had been sitting untracked.

Re-run after any packaging change: python3 scripts/audit-packages.py
2026-05-27 07:14:48 +02:00
675fcd482f docs: codify dual-vhost MUST for modules with a real web UI
Anchor the pattern that secubox-lyrion v1.1.0 demonstrated (admin
static page on the canonical hub vhost + real app served at the root
of its own dedicated vhost) as a REQUIRED rule across the docs:

- docs/MODULE-GUIDELINES.md §4 — new "REQUIRED: dual-vhost pattern"
  subsection with the URL table, the JS pattern for reading the
  public URL from /access, and the explicit forbidden anti-pattern
  (`proxy_pass /<module>/ → app LXC`).
- docs/MODULE-GUIDELINES.md §5 — nginx template updated: `/<module>/`
  is `alias` (static), the dedicated vhost is a separate file. The
  previous "Iframe pattern for LXC web UIs" + "LXC daemon iframe
  target" sections are flagged deprecated.
- .claude/PATTERNS.md Pattern 12 — same rule reproduced for the
  Claude-facing pattern catalog with the source-of-truth JS snippet.
- .claude/HISTORY.md + .claude/WIP.md — entry for today's three-
  module alignment (lyrion 1.1.0 + zigbee + authelia).
- wiki/Architecture.md — short reference + pointer back to
  MODULE-GUIDELINES.
- packages/secubox-lyrion/README.md — adds the URLs table at the top.
- packages/secubox-zigbee/README.md — was a verbatim copy of the
  lyrion README, rewritten as a proper zigbee README with the new
  URLs table + API surface + ports + files.
- packages/secubox-authelia/README.md — Quickstart points at
  sso.gk2.secubox.in (was auth.maegia.tv); URLs table added.

The forbidden anti-pattern catches LMS Material, Nextcloud,
Grafana, z2m and any future module whose UI uses absolute asset
URLs — incident on gk2 2026-05-24, see secubox-lyrion 1.1.0
changelog.
2026-05-24 10:06:56 +02:00
CyberMind
ff990ba112
docs(mochabin): EP06 mPCIe GPIO investigation runbook + hardened probe script (closes #345) (#359)
* docs(mochabin): add EP06 mPCIe GPIO investigation runbook (ref #345)

scripts/probe-mpcie-gpios.sh: empirical CP0 GPIO sweep that drives
each unrequested line HIGH for a few seconds and watches dmesg/lsusb
for new USB device. Skips [used] lines, uses libgpiod gpioset
--mode=time so the line auto-restores to input on release. Modes:
--baseline (snapshot only, safe), --line gpiochipN K (single line),
no arg (full sweep gpiochip1+gpiochip2).

docs/hardware/mochabin-mpcie-ep06-runbook.md: hypothesis, procedure,
and DTS rfkill-gpio template patterned after cn9132-clearfog.dts
lines 69-122. Apply once a candidate GPIO is identified.

--baseline smoke-tested on gk2 — sweep deferred until spare EP06
arrives (no mPCIe device in J5 = no signal regardless of which line
drives W_DISABLE#).

* docs(mochabin): drop EP06 spare assumption from runbook (ref #345)

No spare EP06 on order; the existing EP06-E will be used for the
sweep when re-seated in J5. Replaces "deferred until spare" framing
with a multimeter-scoping fallback for the inconclusive case.

* fix(mpcie): v0.2.0 probe-mpcie-gpios.sh — drop blanket sweep, danger-list, dry-run default (ref #345)

The v0.1.0 "drive every unused-input CP0 GPIO HIGH for 3s one-at-a-time"
sweep took gk2 hard-down within seconds (host became unreachable; ARP
stopped responding). gpioinfo's [used] tag only reflects lines the
kernel *requested* — several unrequested CP0 GPIOs are physically
wired to critical board functions (eth switch reset, PCIe2 PERST#,
pca9554 IRQ) and driving them HIGH wedged the board.

v0.2.0:
- No blanket-sweep mode. Removed.
- Default mode = dry-run candidate listing only. No GPIO is driven.
- --commit --line CHIP N required to actually drive a single line.
- Hardcoded DANGER_LINES (gpiochip1:1, gpiochip1:9, gpiochip1:27)
  skipped unconditionally, even with --commit.
- gpiochip2 entirely held off without --allow-chip2 (no per-line
  schematic mapping for that bank).
- Hard refusals are vocal (REFUSED: ... reason) before exit.

Runbook updated to walk through the new dry-run → --commit flow.
HISTORY documents the incident + the v0.2.0 mitigations.
Lesson saved in memory feedback_gpio_blanket_sweep_crashed_board.md.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-22 17:50:33 +02:00
CyberMind
0ca16778cc
feat(secubox-sentinelle-gsm): v0.3.1 — L3 decode + scoring engine + baseline + qualified alerts (closes #349) (#350)
* docs(plan): #349 sentinelle-gsm v0.3.1 L3 + scoring + baseline (ref #349)

* feat(sentinelle-gsm): GsmtapListener exposes raw_l3 payload bytes (ref #349)

v0.3.0's Observation carried only header metadata (arfcn, frame_nr,
channel, sub_type), which was enough to prove the GSMTAP pipe but not
to drive L3-aware scoring. v0.3.1 needs the post-header payload to
decode BCCH System Information (LAC/CI/MCC/MNC) and CCCH paging
requests (IMSI/TMSI for the privacy-hashed subscriber digest).

Slice the L3 payload from the datagram via data[hdr["hdr_len"]:] in
_on_datagram() and surface it as a new Observation field
`raw_l3: bytes = b""`. Default empty bytes keeps every existing
caller backward compatible — only downstream decoders that opt in
will consume it (wired in Task 5).

Test: existing test_listener_receives_a_datagram now appends a known
18-byte payload and asserts obs.raw_l3 matches verbatim.

* feat(sentinelle-gsm): l3_decode — BCCH SI + CCCH paging request parsing (ref #349)

Pure-Python TLV decoder for the minimal L3 subset needed by the
IMSI-catcher scoring engine: BCCH System Information Types 3/4/6 and
CCCH Paging Request Types 1/2/3. No scapy dependency — the parser
operates directly on the post-GSMTAP-header bytes exposed by
Observation.raw_l3 (Task 1).

Privacy invariant : the public dataclasses (CellInfo, PagedIdentity,
ParsedPagingRequest, ParsedFrame) NEVER carry plaintext IMSI/TMSI/IMEI.
The internal _try_extract_mobile_id() returns plaintext bytes;
_parse_paging() immediately hands them to Anonymizer.anonymize() and
only the HMAC-truncated subscriber_hash escapes into PagedIdentity.
The plaintext goes out of scope at function return.

Permissive parsing : every helper returns an empty CellInfo() / None on
unexpected layouts rather than raising — real GSMTAP frames have
surprising structural variants and exceptions in the consume loop would
crash livemon ingestion.

Tests : 9 cases (6 from the plan + 3 added for coverage). BCD
encoder/decoder round-trip helpers build synthetic frames
programmatically rather than hand-crafting bytes, which also validates
the TS 24.008 §10.5.1.3/4 nibble ordering.

* feat(sentinelle-gsm): baseline.py — operator-baseline learning + cell_baseline table (ref #349)

Adds CellBaseline wrapper + cell_baseline table colocated in observations.db.
Cells graduate to baseline once observed >= LEARN_THRESHOLD (default 3) times.

Key design choices:
* LEARN_THRESHOLD = 3 by default — three sightings before a cell is
  considered part of the operator's legitimate baseline.
* set_learn_mode(seconds) skips the count entirely — every cell observed
  inside that explicit-learn window is INSERTed with learn_count = 3 and
  graduates on first sighting (used by the operator's calibration sweep).
* CellBaseline accepts a raw sqlite3.Connection (NOT an ObservationsDB
  instance) so it stays decoupled from observations.py's public surface
  while still sharing the same .db file.
* cell_baseline table is created idempotently (IF NOT EXISTS) in
  ObservationsDB.__init__, so pre-existing observations.db files keep
  working after upgrade.

Privacy invariant: BaselineCell + cell_baseline have NO subscriber-id
columns — paged identities never enter the baseline path.

Tests (5/5 passing):
  - test_first_consider_starts_at_1_not_baseline_yet
  - test_three_considers_graduate_to_baseline
  - test_learn_mode_graduates_first_consider
  - test_consider_updates_metadata_via_coalesce
  - test_list_orders_by_last_learned_desc

Full pkg sweep: 62 passing (57 prior + 5 new).

* feat(sentinelle-gsm): scoring_engine — 8 heuristics + thresholds (ref #349)

Replaces the v0.1 shape-only scoring.py stub with the full v0.3.1 detector
brain. ScoringEngine wires against CellBaseline + L3Decode (Tasks 2-3)
and runs every parsed frame through 8 heuristics, summing the triggered
contributions into a CellScore clamped to [0, 100].

Heuristics + default scores (sum-clamped to 100):

  1. cipher_downgrade        40   A5/X observed < baseline.cipher_a5
  2. ghost_bts                35   cell unknown to baseline + paging seen
  3. identity_mismatch       30   observed (mcc, mnc) != baseline pair
  4. relocalization_storm    25   > 3 distinct LACs in 60s on cell
  5. identity_request_abuse  35   > 2 paging reqs in 300s per hash
  6. anomalous_neighbours    15   neighbour list went non-empty -> empty
  7. t3212_out_of_band       15   T3212 outside [1, 60] min
  8. orphan_arfcn            20   FR carrier announces ARFCN outside plan

Rolling-window state (in-memory only, no DB persistence in v0.3.1):
  _lac_window         cell_id          -> deque[(ts, lac)]
  _idreq_window       subscriber_hash  -> deque[ts]
  _neighbour_baseline cell_id          -> set[int]   (cumulative ARFCNs)

Thresholds:
  * DEFAULT_THRESHOLDS class constant carries per-heuristic config dicts.
  * update_thresholds() does a per-heuristic deep-merge so callers can
    pass partial updates like {"cipher_downgrade": {"score": 55}} without
    clobbering "enabled" or any other unspecified field.
  * thresholds() returns a deep-copy so callers can't mutate engine state.

Privacy invariant kept: identity_request_abuse keys windows on
subscriber_hash strings (HMAC-trunc, supplied by l3_decode.Anonymizer),
never plaintext IMSI/TMSI. Reasons truncate the hash to 8 chars defensively.

France GSM-900 ARFCN plan is approximate and permissive: unknown
(mcc, mnc) pairs return triggered=False to avoid false-positives.

Dropped lib/sentinelle_gsm/scoring.py — the v0.1.0 shape-only stub
(empty Baseline + score()=0) is replaced. No other module imported it.

Tests:
  * api/tests/test_scoring_engine.py — 11 tests, one per heuristic
    (plus permissive-unknown-carrier guard), one for evaluate()
    aggregation/clamping, one for update_thresholds() deep-merge.
  * Full sweep: 73 passed (was 62; +11 new).

* feat(sentinelle-gsm): consume loop wires L3 + scoring + trusted match → Alert (ref #349)

- Extracted `_process_observation(obs)` helper for testability — the consume
  coroutine now delegates per-frame work so tests can drive synthesized
  Observations through the full pipeline without binding UDP.
- New endpoints: GET /baseline, POST /baseline/learn (sweep window),
  GET /scoring/thresholds, POST /scoring/thresholds (deep-merge).
- Audit log entry on POST /scoring/thresholds via the existing
  `secubox.sentinelle-gsm.api` logger — surfaces in /journal/stream.
- Alert emission decision tree:
    score < threshold        → drop (no write)
    score >= threshold + match → one labeled alert per matched trusted phone
    score >= threshold + no match → single anomaly-only alert
- ALERT_THRESHOLD configurable via SENTINELLE_ALERT_THRESHOLD env (default 60).
- TrustedRegistry now hashes the IMSI as `imsi.encode("ascii").hex()` so
  the persisted token matches the canonical form produced by the L3
  decoder's paging-request path. Existing trusted tests don't pin the
  hash value, so behaviour is unchanged for read-back / lookup-by-id.
- 5 new integration tests at api/tests/test_alert_emission.py cover the
  below-threshold drop, anomaly-only emission, trusted-match labeling,
  paging-event hash persistence, and audit-log emission on threshold
  update. Full sweep 78/78 passing.

* feat(sentinelle-gsm): webui baseline + scoring panels + trusted_label chip (ref #349)

Adds two new webui panels and a trusted-label chip on the alerts row:

- #baseline panel: Cell ID / MCC / MNC / LAC / ARFCN / cipher / learn count
  / last learned table backed by GET /api/v1/sensor/gsm/baseline. "Start
  learn (5 min)" button arms POST /baseline/learn?sweep_seconds=300 and
  re-polls the table every 15s for the 5-minute window so cells appear as
  they graduate.

- #scoring panel: per-heuristic enable + score input (0..100, clamped
  client-side) backed by GET/POST /scoring/thresholds. Form values are
  cached in _thresholdState so any backend-only fields (heuristic-specific
  knobs) survive the POST round trip.

- Alerts table: trusted_label is now rendered as a violet pill via the
  new .trusted-chip class (replaces the older .target-pill markup for
  the same data path).

Local navigation gets two new anchors (Baseline, Scoring) between
Observations and Actions. The 10s scan poll now also refreshes baseline
while a scan is running.

Wires 4 new event listeners (refresh-baseline, baseline-learn,
refresh-scoring, save-scoring). No backend changes; all endpoints already
exist (Tasks 1..5).

Tests: 78/78 passing.

* chore(sentinelle-gsm): bump 0.3.1 + extend privacy invariant tests (closes #349)

Append 4 new structural privacy tests under tests/test_privacy_invariant.py
to lock the v0.3.1 invariants in place:

  * test_l3_decode_returns_no_plaintext_fields — CellInfo, PagedIdentity,
    and ParsedPagingRequest dataclasses must not carry any plaintext
    subscriber-id field (imsi/tmsi/imei/msisdn/iccid/subscriber_id).
  * test_paging_request_hashes_paged_identities — feeds a synthetic
    Paging Request Type 1 with a known TMSI through L3Decode and asserts
    the plaintext TMSI bytes (as hex) NEVER appear in the resulting
    PagedIdentity.subscriber_hash.
  * test_cell_baseline_has_no_subscriber_fields — the BaselineCell
    dataclass must remain operator-side metadata only: no subscriber_hash,
    no plaintext identifier.
  * test_scoring_engine_reasons_contain_no_plaintext_imsi — the free-text
    reason strings produced by ScoringEngine.evaluate() must never echo
    a 15-digit token (plaintext-IMSI shape) for typical observations.

These four tests pass against the v0.3.1 modules (Tasks 1-6) with no
code change — the modules were designed for these invariants from the
start, this commit just locks them in regression-test form.

Full sweep: 78 -> 82 passing.

Changelog: 0.3.0 -> 0.3.1 with the v0.3.1 entry summarising the new
l3_decode, baseline, scoring_engine modules, the consume-loop wiring
that emits qualified Alerts with trusted_label, the webui panels, and
the updated test breakdown (l3_decode x9, baseline x5, scoring_engine
x11, alert_emission x5, privacy x4 = 34 new, full sweep 82/82).

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-22 14:34:54 +02:00
CyberMind
5f3d268c05
feat(secubox-sentinelle-gsm): v0.3.0 — passive observation pipeline (closes #344 #347) (#348)
* docs(plan): #347 sentinelle-gsm v0.3.0 observation pipeline (ref #344 #347)

* feat(sentinelle-gsm): livemon_runner — managed grgsm subprocess (closes #344 ref #347)

Introduces LivemonRunner, the asyncio subprocess manager that owns the
single grgsm_livemon_headless child for v0.3.0.

Device-claim invariant (the load-bearing point that closes #344): the
parent service NEVER opens the RTL-SDR itself. The RTL-SDR is claimed
ONLY by the spawned grgsm_livemon_headless child, and ONLY for the
lifetime of an active scan. start() spawns with `--args=rtl=0 -f <freq>`
so gr-osmosdr binds RTL-SDR instead of its UHD default; stop() sends
SIGTERM and falls back to SIGKILL after a 5 s timeout, releasing the
device cleanly.

Concretely this means ad-hoc tooling (rtl_test, rtl_adsb, kalibrate)
keeps working alongside an idle secubox-sentinelle-gsm service — the
race the v0.1 livemon.py USB-detect stub created is gone, because the
parent has no reason to touch the dongle outside an explicit scan.

Stderr from the child is drained into a 16 KiB-capped ring buffer so
status() can surface the last 2 KiB without unbounded memory growth,
and double-start() raises RuntimeError instead of silently leaking
processes.

Tests cover: initial-state, RTL args propagation to create_subprocess_exec,
double-start refusal, SIGTERM-on-stop + state clear, and stop-as-noop
when nothing is running. asyncio.create_subprocess_exec is fully mocked
— no real grgsm process is spawned by the test suite.

Test sweep: 5 new + 29 existing = 34 passing.

* feat(sentinelle-gsm): gsmtap_listener — async UDP 4729 + GSMTAP v2 parse (ref #347)

Task 2 of v0.3.0 observation pipeline.

- `lib/sentinelle_gsm/gsmtap_listener.py`: asyncio DatagramProtocol bound
  to 127.0.0.1:4729 (configurable). Tiny manual 16-byte GSMTAP v2 header
  parser — no scapy runtime dep on the hot path. scapy stays reserved
  for the deeper L3 decode (BCCH/CCCH paging) landing in v0.3.1.
- `Observation` dataclass exposes ts/arfcn/frame_nr/channel/sub_type plus
  optional lac/ci/mcc/mnc/cell_id/subscriber_hash. No plaintext-id field;
  `subscriber_hash` is HMAC-only (populated when L3 decode lands).
- Bounded queue (maxsize=2048); on QueueFull we drop silently —
  observations are stochastic, backpressure to the radio would be worse.
- 4 tests: minimum-valid parse, too-short reject, wrong-version reject,
  end-to-end UDP roundtrip on port 47291 (avoids colliding with a
  running grgsm on 4729).

All tests pass: 4/4 new, 38/38 sweep.

* feat(sentinelle-gsm): observations.py — SQLite sightings + paging_events (ref #347)

Adds ObservationsDB persistence layer for the v0.3 observation pipeline.

* Sighting (cell_id PK, mcc/mnc/lac/ci/arfcn, first_seen/last_seen,
  sighting_count) — UPSERT semantics: INSERT on new cell_id, else bump
  sighting_count + refresh last_seen with COALESCE on optional fields so
  partial updates don't clobber previously-observed values.
* PagingEvent (ts, cell_id, subscriber_hash, request_type) — seeds the
  paging history table that v0.3.1 will hash-match against trusted_phones.
  Schema deliberately omits plaintext IMSI/TMSI/IMEI fields.
* _guard_plaintext() mirrors the regex guard in alert_sink.AlertSink.write
  (same \b\d{15}\b pattern, same "plaintext-IMSI shape detected — refusing
  write" error contract). Applied on every write path: upsert_sighting
  guards cell_id; record_paging guards both cell_id and subscriber_hash.
* Two indexes on paging_events (ts, cell_id) for the v0.3.1 lookup path.
* 4 tests cover the contract: new-insert, bump-count, hash accept, and
  the plaintext-IMSI refusal. Full sweep: 42 passing (38 prior + 4 new).

* feat(sentinelle-gsm): API — /scan/start /scan/stop /scan/status /observations (ref #347)

Task 4 of the v0.3.0 observation-pipeline plan. Wires the LivemonRunner +
GsmtapListener + ObservationsDB introduced in Tasks 1–3 to four new HTTP
endpoints behind the existing nginx + Authelia JWT gate.

  - POST /scan/start  {freq}  → bind GSMTAP UDP listener, spawn
                                grgsm_livemon_headless on freq, start a
                                background consume task that drains
                                Observations into the SQLite store
  - POST /scan/stop           → cancel the consume task FIRST so no
                                orphan subprocess is left behind, then
                                SIGTERM the runner, then close the
                                listener socket (the CHILD owns the
                                RTL-SDR, never the parent)
  - GET  /scan/status         → current runner state (running / pid /
                                freq / started_at / stderr_tail)
  - GET  /observations?limit  → recent cell sightings, newest first;
                                limit clamped to [1, 1000], default 200

Synthetic cell_id formation until v0.3.1's L3 BCCH decode lands:
`f"arfcn-{obs.arfcn}-ch-{obs.channel}"`. The ObservationsDB schema
already accepts NULL for the operator quadruple (mcc/mnc/lac/ci) so
nothing downstream has to change when the real cell_id arrives.

Three new tests in api/tests/test_scan_api.py exercise the
happy path with mocked LivemonRunner + mocked GsmtapListener (so no
gr-gsm binary is spawned and no UDP socket is bound) plus a real
ObservationsDB under tmp_path so GET /observations exercises the
real SQLite path. Fixture clears `_consume_task` between tests to
keep them order-independent.

Test sweep: 42 (existing) + 3 (this) = 45 passing.

* feat(sentinelle-gsm): webui scan control + observations panels (ref #347)

Adds two new panels to the standalone MIND webui:

* Scan control — Start/Stop buttons, freq input (default 925.4M = E-GSM 900
  ARFCN 975), 4-cell status row (state dot, freq, pid, started_at), and
  a collapsible <details> showing the last 2 KB of stderr from
  grgsm_livemon_headless. Start sends {freq: <value>} to /scan/start.
* Observations — live table of cell sightings with columns Cell ID,
  ARFCN, MCC, MNC, LAC, CI, First seen, Last seen, Sightings. Badge in
  the panel header tracks the current count.

JS additions (sentinelle.js):

* loadScanStatus / startScan / stopScan / loadObservations helpers
* 3 new event listeners (btnScanStart, btnScanStop, btnRefreshObs)
* Background interval (10s) that polls /scan/status always and
  /observations only while a scan is running — keeps the post-scan
  terminal state on screen without hammering sqlite.

CSS additions (sentinelle.css):

* .scan-status-row (4-col grid inside a panel, matches .status-strip)
* .scan-stderr / .scan-stderr-details (monospace tail block, MIND palette)
* Responsive collapse to 2 cols then 1 col matching the existing breakpoints.

API surface untouched — Tasks 1-4 already shipped /scan/start, /scan/stop,
/scan/status and /observations under /api/v1/sensor/gsm/. Test sweep still
45 passing.

* build(sentinelle-gsm): merge detect_rtlsdr_usb into livemon_runner; drop v0.1 stub; add scapy + gr-gsm deps (ref #344 #347)

Co-locate RTL-SDR USB detection with the grgsm_livemon orchestrator.

- Move RTLSDR_USB_IDS + detect_rtlsdr_usb() from livemon.py into
  livemon_runner.py. The function is pure sysfs (reads
  /sys/bus/usb/devices/*/idVendor + idProduct) — it does NOT claim
  the USB device, so it was never the source of #344. The #344 bug
  was fixed by Task 1's LivemonRunner model (child-only device claim
  via subprocess.create_subprocess_exec).
- Delete lib/sentinelle_gsm/livemon.py — the v0.1 stub also carried a
  useless NotImplementedError docstring for listen_gsmtap_udp, fully
  superseded by gsmtap_listener.py (Task 1).
- api/main.py: collapse the two-line "from sentinelle_gsm.livemon ...
  / from sentinelle_gsm.livemon_runner ..." block into a single import
  pulling LivemonRunner + detect_rtlsdr_usb from livemon_runner.
- debian/control: promote python3-scapy from Recommends to Depends
  (runtime requirement once the v0.3.1 L3 decode path ships); keep
  gr-gsm in Recommends (subprocess that runs scans).

dpkg-buildpackage: ok (secubox-sentinelle-gsm_0.2.3-1~bookworm1_all.deb).
dpkg-deb -c | grep livemon: only livemon_runner.py + test_livemon_runner.py,
no v0.1 stub. pytest api/tests/ tests/: 45 passed.

* chore(sentinelle-gsm): bump 0.3.0 + extend privacy invariant tests (closes #344 #347)

Privacy invariant coverage extended for the v0.3.0 observation pipeline:

  * test_observation_has_no_plaintext_fields — Observation dataclass
    (gsmtap_listener) exposes ONLY subscriber_hash, never plaintext
    IMSI/TMSI/IMEI/MSISDN/ICCID/subscriber_id.
  * test_paging_event_only_stores_hash — PagingEvent dataclass
    (observations) has no plaintext-identifier fields and exposes
    subscriber_hash.
  * test_observations_db_refuses_plaintext_imsi — ObservationsDB.record_paging
    raises ValueError("plaintext-IMSI ...") when handed a 15-digit
    subscriber_hash (write-time shape guard).

Version bump 0.2.3 → 0.3.0 with the full v0.3.0 changelog:
livemon_runner.py (RTL-SDR subprocess wrapper, closes #344) +
gsmtap_listener.py (asyncio UDP 4729) + observations.py (SQLite
sightings/paging_events) + /scan API + webui panels + scapy promoted
to Depends + dropped v0.1 livemon.py stub. 48/48 tests passing.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-22 13:43:54 +02:00
CyberMind
95550edc4e
feat(secubox-sentinelle-gsm): v0.2.0 — standalone WebUI + local alerts (SSE + browser notification) (closes #340) (#341)
* docs(plan): #340 secubox-sentinelle-gsm v0.2 standalone webui + local alerts (ref #340)

* feat(sentinelle-gsm): alert_sink — SQLite history + SSE broadcast hub (ref #340)

Introduces lib/sentinelle_gsm/alert_sink.py: a SQLite-backed alert
history with an in-memory asyncio pub/sub hub that fans new alerts
out to live Server-Sent Events subscribers.

The Alert dataclass carries only privacy-safe fields — `subscriber_hash`
is HMAC-truncated (Anonymizer), never plaintext. AlertSink.write()
enforces this with a load-bearing privacy guard: any string field
matching `\b\d{15}\b` (plaintext IMSI shape) triggers ValueError and
the row is refused. SQLite is opened with check_same_thread=False so
the FastAPI app can read/write from multiple coroutines safely.

Adds api/tests/test_alert_sink.py with 5 tests covering write/list
roundtrip, the plaintext-IMSI privacy guard, HMAC acceptance, live
subscriber fan-out, and SSE chunk format. 5 passed locally.

* feat(sentinelle-gsm): trusted phones registry — HMAC-hashed IMSI + label (ref #340)

Privacy contract:
- Plaintext IMSI is accepted by add() ONCE, immediately HMAC-hashed via
  the existing sentinelle_gsm.observer.Anonymizer.anonymize() helper.
- The plaintext never persisted, never logged, never returned by any
  method. After add() returns, plaintext_imsi is out of scope.
- The on-disk store contains only {id, imsi_hash, label, added_at}.

Storage format: JSON via the stdlib `json` module instead of TOML.
`python3-tomli-w` (the only writer for the new tomllib reader) is not
in Debian bookworm — it first ships in trixie. python3-toml (legacy)
writes TOML but is deprecated. JSON is stdlib, atomically renderable,
and human-grep-able, which is what a trusted-phones registry needs.

Anonymizer integration: uses .anonymize() (HMAC-SHA256, 16 hex chars
truncated). The plan referenced .hash_subscriber_id() — that method
does not exist on Anonymizer in observer.py; .anonymize() is the
canonical hashing entry point.

Tests: 6/6 passing
- test_add_hashes_and_does_not_store_plaintext (privacy invariant
  enforced: plaintext IMSI never appears on disk)
- test_lookup_by_hash_finds_added_phone
- test_lookup_by_hash_returns_none_for_unknown
- test_delete_removes_entry
- test_invalid_imsi_rejected
- test_persistence_across_instances

* feat(sentinelle-gsm): API — /alerts (history + SSE + test) + /trusted CRUD (ref #340)

Adds 6 endpoints under the existing /api/v1/sensor/gsm/ prefix
(nginx-added; mounted at root of the FastAPI app):

  GET    /alerts          — paginated SQLite-backed history
                            (query: ?limit=100&since=<epoch>)
  GET    /alerts/stream   — SSE live feed; Cache-Control: no-cache,
                            X-Accel-Buffering: no, Connection: keep-alive
                            so nginx and HTTP caches don't buffer it
  POST   /alerts/test     — operator manual trigger; ValueError from
                            the AlertSink privacy guard (plaintext-IMSI
                            shape) surfaces as HTTP 500
  GET    /trusted         — list trusted phones (HMAC + label, no plaintext)
  POST   /trusted         — body {imsi, label}; ValueError → 400
  DELETE /trusted/{id}    — unknown id → 404

Wiring:
  * AlertSink + TrustedRegistry are module-level singletons initialised
    on FastAPI startup so tests can override via monkeypatch.
  * `require_jwt` is a no-op dependency stub — real JWT enforcement is
    done by nginx + Authelia in front of the Unix socket. The stub
    exists so test code (and future host-direct callers) can swap auth
    via `app.dependency_overrides[require_jwt]`.
  * Anonymizer is loaded via a new `_get_anonymizer()` helper that
    reads /etc/secubox/secrets/sentinelle-gsm-hmac when present and
    falls back to an ephemeral key otherwise.
  * trusted.json path is /etc/secubox/sentinelle-gsm/trusted.json
    (the Task-2 deviation from the plan's TOML; bookworm lacks
    python3-tomli-w).
  * version bumped 0.1.0 → 0.2.0 in the FastAPI metadata only;
    the debian/changelog bump lands in Task 6.

Tests (7 new in api/tests/):
  * test_alerts_api.py — POST /alerts/test happy path, GET history
    roundtrip, plaintext-IMSI guard → 500, SSE route registration
    (via app.routes + /openapi.json, not by opening the stream — the
    async generator awaits forever on q.get(), incompatible with sync
    TestClient iteration; SSE chunk format is covered by
    test_alert_sink.py::test_stream_emits_sse_format).
  * test_trusted_api.py — add/list/delete roundtrip with hash-not-
    plaintext invariant, invalid IMSI → 400, unknown id → 404.

Run: pytest api/tests/test_alerts_api.py api/tests/test_trusted_api.py
Result: 7 passed.

* feat(sentinelle-gsm): standalone webui — live alerts + browser Notification API (ref #340)

Adds the Task 4 standalone WebUI for sentinelle-gsm at
packages/secubox-sentinelle-gsm/www/sentinelle/:

- index.html — canonical SecuBox scaffold (body flex, aside.sidebar 220px
  fixed with section anchors, main reserves the global menu bar with
  padding-top: calc(48px + 1.5rem)). Four panels: status strip, alerts
  table (aria-live=polite), trusted phones table, actions.
- sentinelle.css — MIND palette (--mind-violet: #3D35A0) per Charte
  SecuBox, Space Grotesk titles + JetBrains Mono data/code, modal with
  z-index 1500 (above the global-menu-bar z-index 998), toast banner,
  score chip colour grading (low/med/high), responsive breakpoints.
- sentinelle.js — vanilla single-IIFE module, no framework, no CDN.
  EventSource consumer of /api/v1/sensor/gsm/alerts/stream (auto-reconnect
  surface), CRUD wrappers for /alerts /alerts/test /trusted /trusted/{id},
  Browser Notification API (requested from a user-gesture handler), beep
  synthesised on-the-fly via Web Audio API (no asset), localStorage mute
  toggle, modal-based add flow, ESC/backdrop close, accessible aria-live
  region for the alerts list.

All API calls hit root-relative /api/v1/sensor/gsm/* — Task 5 will mount
the nginx route. Local smoke test booted uvicorn against the API and
verified the HTML/CSS/JS serve and all four CRUD/test routes respond as
the UI expects.

* feat(sentinelle-gsm): nginx route /sentinelle/ + MIND menu entry + .install (ref #340)

* nginx/sentinelle-webui.conf: alias for /sentinelle/ (static webui) +
  SSE-tuned override for /api/v1/sensor/gsm/alerts/stream. Uses `^~`
  literal-prefix match so it wins precedence over the shorter
  /api/v1/sensor/gsm/ prefix in sentinelle-gsm.conf and against any
  future regex locations in the same server scope. Re-sets
  proxy_read_timeout / proxy_send_timeout to 24h after the common
  secubox-proxy include (which forces 30s) so long-lived SSE
  connections survive.
* menu.d/45-sentinelle.json: MIND-category entry (icon: satellite,
  order: 45). Category emitted as lowercase `mind` to match the
  canonical schema used by every other package menu.d entry.
* debian/secubox-sentinelle-gsm.install: ship the new files. The
  package uses a hand-rolled override_dh_auto_install in debian/rules,
  so the install file is documentation; debian/rules is extended
  with explicit cp steps for nginx/sentinelle-webui.conf and
  www/sentinelle/. menu.d already had a wildcard copy.

* chore(sentinelle-gsm): bump 0.2.0 + extend privacy invariant tests (closes #340)

- Append 3 new privacy invariant tests:
  * Alert dataclass has no plaintext IMSI/IMEI/TMSI/MSISDN/ICCID fields
  * TrustedPhone dataclass only stores imsi_hash (never plaintext)
  * AlertSink.write() raises ValueError on any 15-digit token in a
    textual field — non-regression guard for the OPAD-doctrine privacy
    invariant.
- Bump debian/changelog to 0.2.0-1~bookworm1 with the full Task 1-6
  feature summary: alert_sink, trusted registry, REST routes, WebUI,
  nginx + Hub menu integration. Notes the JSON-over-TOML deviation
  (python3-tomli-w not in bookworm).
- 29/29 tests pass: 5 alert_sink + 4 alerts_api + 3 trusted_api +
  6 trusted_registry + 11 privacy_invariant (8 prior + 3 new).

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-22 11:49:05 +02:00
CyberMind
832141cd0a
feat(secubox-streamlit): v1.2.2 — per-app idle timeout + wake-on-demand (closes #331) (#334)
* docs(plan): #331 streamlit idle-timeout + wake-on-demand (ref #331)

* feat(streamlit): add idle-tracking helpers to streamlitctl (ref #331)

Add the foundation for per-app idle timeout / wake-on-demand:

- IDLE_STATE_DIR constant at /var/lib/secubox/streamlit/idle
- _idle_config: read [idle] settings from /etc/secubox/streamlit.toml
  with safe defaults
- _app_running: check whether an app is listening on its port INSIDE
  the LXC (existing inline ss check is host-side and unreliable)
- _app_active_conns: count ESTABLISHED connections to an app's port
- _app_last_active / _app_touch_active: per-app last-activity epoch
  tracked via state-file mtime

Wire cmd_app_start to touch the state file on every successful start
so a freshly-started app does not get immediately reaped by the
upcoming idle-check.

All helpers degrade gracefully when the LXC is down (lxc_running
guard) and the helpers themselves do not change any existing
behaviour — they are purely additive.

* feat(streamlit): streamlitctl app idle-check — stop idle apps past timeout (ref #331)

Add cmd_app_idle_check that iterates apps under $APPS_PATH and stops
those that have had zero ESTABLISHED connections for longer than the
configured timeout_minutes (default 30, from [idle] in streamlit.toml).
Apps with active connections get their state file touched; apps with no
state file yet are touched too (grace period). Master switch via [idle]
enabled in /etc/secubox/streamlit.toml.

Wire idle-check + wake into the `app` subcommand dispatcher; wake's
function (cmd_app_wake) lands in the next commit (dead branch until
then). Usage line updated to include both new subcommands.

* feat(streamlit): streamlitctl app wake — lazy start + poll for port (ref #331)

Adds cmd_app_wake <name> [wait_seconds] (default 30s):
- returns 0 if already running OR if the app comes up within wait_seconds
- returns 1 on start failure or poll timeout
- returns 2 on misuse (missing name, unknown app)

Uses _app_running for readiness probing and _app_touch_active to refresh
the idle marker on a successful wake. Polls every 1s for portability.

The case-block dispatcher (idle-check / wake) was already wired in Task 2.

* feat(streamlit): systemd idle-sweep timer (every 5 min) (ref #331)

Ship secubox-streamlit-idle.service (oneshot, runs
`streamlitctl app idle-check`) and secubox-streamlit-idle.timer
(OnBootSec=5min, OnUnitActiveSec=5min) so idle Streamlit apps are
auto-suspended without a separate cron.

Wire the units through debian/secubox-streamlit.install (the package
uses a hand-rolled override_dh_auto_install, so dh_installsystemd's
auto-detection by package.suffix name doesn't pick them up — explicit
.install is needed). Enable/disable the timer in postinst/prerm
alongside the main service.

* feat(streamlit): POST /apps/{name}/wake — lazy start API endpoint (ref #331)

* feat(streamlit): default [idle] config block (timeout 30 min) (ref #331)

* chore(streamlit): bump to v1.2.2 with idle-timeout + wake summary (closes #331)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-22 07:58:33 +02:00
e562446383 docs(superpowers): capture 3 specs from operator drop 2026-05-20
* secubox-zigbee-mqtt-iot-stack — v2.4.0 rewrite-in-place spec for the
  IoT radio stack (Mosquitto, zigbee2mqtt, Sonoff CC2652P, 4R buffer)
* secubox-iot-hub-webui — multi-connector WebUI dashboard spec
  (offline-first, vanilla JS, FastAPI router /api/v1/iot-hub)
* secubox-sso-bridge — SSO doctrine (Authelia LXC + auth_request
  pattern for SSO-less backends, health-banner percolation)

Saved verbatim from the operator briefing. Implementation deferred
until v2.4.0 alignment is reached on the underlying modules.
2026-05-20 12:54:22 +02:00
CyberMind
a58b6fa55d
fix(lxc-modules): 9 board-deployment fixes for grafana/yacy/rustdesk install-lxc.sh + nginx wiring (#238)
Field-deploying v2.11.0's three new LXC modules on arm64 MOCHAbin
(gk2 192.168.1.200) surfaced nine bugs that the local smoke build
doesn't exercise (no lxc-create in dpkg-buildpackage). All three
modules now reach overall=green on the board with the canonical hub
vhost (admin.gk2.secubox.in) routing /api/v1/<m>/* and /<m>/ correctly.

install-lxc.sh (all 3 modules)
  1. lxc-create -t download (the legacy `debian` template fails on
     bookworm unprivileged containers).
  2. ensure_masquerade() — nftables MASQUERADE rule for 10.100.0.0/24,
     idempotent. systemd-networkd's IPMasquerade=ipv4 doesn't land on
     appliances using ifupdown/NetworkManager.
  3. ensure_resolv() — seed /etc/resolv.conf via lxc-attach AFTER
     start_lxc + wait_for_network (rootfs is owned by mapped uid 100000,
     host root can't write directly). Unlink the symlink the download
     template ships (→ /run/systemd/resolve/stub-resolv.conf which
     doesn't exist before systemd-resolved is up).

install-lxc.sh (grafana)
  4. gpg --dearmor on apt.grafana.com key (signed-by= needs binary,
     wget delivers ASCII-armored).

install-lxc.sh (yacy)
  5. YACY_RELEASE_URL: v1.940 → v1.941 (old URL now returns 404).

install-lxc.sh (rustdesk)
  6. Detect host arch (arm64/amd64/armhf) and pull the matching
     RustDesk server build. Hardcoded amd64 binary failed with
     "Exec format error" on the arm64 MOCHAbin.
  7. Flatten any of amd64/, arm64v8/, armv7/ subdirs the zip ships.

debian/secubox-<m>.service (all 3 modules)
  8. ExecStartPre prefix with `+` (run as root). The mkdir + chown
     pre-steps run as User=secubox by default, chown fails ("Operation
     not permitted") and the service can't start.

debian/rules + nginx/<m>.conf (all 3 modules)
  9. Install nginx snippet to BOTH /etc/nginx/secubox.d/ AND
     /etc/nginx/secubox-routes.d/. The canonical hub vhost
     (sites-enabled/webui.conf serving admin.gk2.secubox.in) only
     includes routes.d/, NOT secubox.d/ which is only included by the
     legacy sites-available/secubox vhost. Also switch the
     /api/v1/<m>/ location from `proxy_pass http://unix:.../sock:/` to
     explicit `rewrite ^/api/v1/<m>/(.*)$ /$1 break; proxy_pass
     http://unix:.../sock;` — matches the canonical pattern used by
     secubox-users; the previous syntax did not strip the prefix so
     FastAPI saw the full path and returned 404 on every route.

Docs

  * docs/MODULE-GUIDELINES.md §7 extended with mandatory lifecycle
    verbs install / repair / wizard / reload / uninstall. Repair detects
    + fixes drift idempotently; wizard chains interactive seed + install;
    uninstall asks for confirmation, removes LXC + state + secrets.
    Implementation in grafanactl/yacyctl/rustdeskctl deferred to v2.12.0
    (currently install + reload are wired, the rest is on the v2.12.0
    backlog).
  * docs/superpowers/specs/2026-05-20-secubox-wall-ep06.md (#236) —
    Quectel EP06-E modem-based rogue-base-station sensor (WALL layer).
  * docs/superpowers/specs/2026-05-20-secubox-sentinelle-gsm.md (#237) —
    RTL-SDR + gr-gsm passive RX-only false-BTS detection (MIND layer,
    feeds WALL/OPAD). Privacy-by-design HMAC-truncated identifiers.
  * .claude/{WIP,TODO}.md refreshed with v2.11.1 fix trail + v2.12.0
    target + P2 sensor backlog.

Package version bump: 1.0.0 → 1.0.7 (grafana/rustdesk) / 1.0.7 (yacy),
all three end at the same .deb revision suitable for v2.11.1 tag.

Validated end-to-end on board gk2:

  grafanactl status  →  overall=green (lxc + grafana-server + host-api all up,
                                       admin/<password> at /etc/secubox/secrets/grafana-admin,
                                       6 dashboards + secubox-metrics datasource provisioned)
  yacyctl    status  →  overall=green (lxc + JVM + YaCy v1.941 standalone,
                                       admin password at /etc/secubox/secrets/yacy-admin)
  rustdeskctl status →  overall=green (lxc + hbbs + hbbr + host-api,
                                       PSK at /etc/secubox/secrets/rustdesk-key,
                                       nftables DNAT 21116/udp installed)

  https://admin.gk2.secubox.in/api/v1/grafana/healthz  →  HTTP 200
  https://admin.gk2.secubox.in/api/v1/yacy/healthz     →  HTTP 200
  https://admin.gk2.secubox.in/api/v1/rustdesk/healthz →  HTTP 200

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-20 09:21:48 +02:00
e170ef98c1 docs: correct LXC bridge name br-secubox -> br-lxc (matches mail/gitea convention) 2026-05-20 06:55:52 +02:00
71f631003d docs: MODULE-GUIDELINES + grafana/yacy/rustdesk LXC plan
- docs/MODULE-GUIDELINES.md (new) — canonical reference for authoring a
  secubox-<module> package: file structure, LXC layout, WebUI conventions,
  nginx wiring, Debian packaging, CTL three-fold, FastAPI shape, tests,
  validation checklist. Sister doc to docs/grammar.md and docs/UI-GUIDE.md.
- docs/grammar.md — cross-reference MODULE-GUIDELINES.md in the header
  sister-docs block.
- docs/MODULES.md — point new-module authors at MODULE-GUIDELINES.md.
- docs/superpowers/plans/2026-05-20-grafana-yacy-rustdesk-lxc-modules.md
  (new) — concrete implementation plan for three new LXC-hosted modules:
  secubox-grafana (#229), secubox-yacy (#230), secubox-rustdesk (#231).
  All three follow the new MODULE-GUIDELINES.md. IP allocations 10.100.0.{70,80,90}.
2026-05-20 06:43:04 +02:00