Commit Graph

2779 Commits

Author SHA1 Message Date
a33e3c0092 docs(spec): alertes CVE récurrentes et réelles
Le module secubox-cve-triage existe déjà et fait plus que prévu (dpkg, NVD,
CVSS, EPSS réel, flux KEV CISA, triage, panel). Il lui manque une gâchette
(aucun .timer), une voix (zéro notify) et un filtre anti-bruit. Ce spec livre
l'appelant, pas un module.

Filtre: KEV ∧ installé ∧ (exposé public ∨ rançongiciel) — un ET, pas un OU.
L'exposition vient de l'inventaire secubox-profiles livré ce jour (16 modules
publics sur 187). Anti-bruit: alerte sur le delta, ré-alerte sur escalade,
acquittement, digest, plancher anti-tempête.

CrowdSec CTI écarté: clé obligatoire, 40 requêtes/mois en gratuit, dépendance
cloud pour une donnée que le KEV CISA fournit librement (1647 CVE, sans clé).

XMPP chiffré honnêtement: le module jabber ne sait pas envoyer: c'est un
gestionnaire Prosody. Le sink exige slixmpp (présent dans bookworm), un compte
bot, un secret et un client — une tâche isolée, pas un branchement.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 15:17:50 +02:00
ea25a23831 Merge feat/profiles-webui: profiles panel + own-socket API + 46s->0.03s status (#871)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 15:16:08 +02:00
b7bd56b23a perf(profiles): batch the probes and cache the status (46s -> 0.03s)
GET /status took 46 SECONDS on the board and the panel was unusable.

Cause: observe() ran 3 subprocesses per module (is-enabled, is-active, show)
plus lxc-info, and load_routes() re-read the routes file once per module. With
187 modules that is ~560 spawns at ~29ms each, on a box running 118 services at
load 5.4 on 4 cores.

observe_all() now issues ONE systemctl-show for every unit (measured: 187 units
in 0.28s), ONE LXC enumeration, and reads routes once. RSS comes from
/proc/<MainPID>/status - a file read, not a spawn. web.py serves /status from a
60s cache refreshed off the event loop (asyncio.to_thread), per the project's
own "double caching" pattern; the CLI still computes live, since a one-shot
command gains nothing from staleness. /status reports cached_at + age_s rather
than pretending to be live.

Two traps, both covered by tests:
- A bare template unit (name@.service) makes systemctl-show fail for the WHOLE
  batch - it returned 40 blocks of 188. Templates are excluded.
- A unit missing from the batch output must become enabled=None/active=None
  (unknown), never False. This branch has fixed five defects of that class; a
  batch is a fresh way to commit the same one.

Measured live on gk2 after deploy: 46s -> 2.57s cold, 0.025s warm; 187 modules;
sockets 108 before and after; panel 1.6ms and API 94ms through nginx.

The honest-unknown rule is visible in the real data: totals went from
off:66/unknown:0 to off:62/unknown:4 - the old code was asserting "off" for the
4 template units it could not actually resolve.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 15:10:17 +02:00
dfb01df1bb fix(profiles): ship the nginx route to the directory nginx actually includes
Two bugs that cancelled into a 404, both found by deploying rather than by tests:

1. The drop-in was installed to /etc/nginx/secubox.d/, which nothing includes.
   The admin vhost includes /etc/nginx/secubox-routes.d/*.conf. secubox.d holds
   138 files and is dead — a route dropped there is inert.

2. proxy_pass ended in ':/', which strips the prefix. The app registers the FULL
   path (@app.get('/api/v1/profiles/status')), so stripping handed it /status —
   a route it does not know. Prefix must be preserved for a dedicated socket
   whose app owns its full path.

Verified live through nginx and public HTTPS: panel and all three API routes 200.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 13:45:21 +02:00
cedc012051 fix(profiles): load_routes must not crash on an untraversable parent
Found by deploying to the real board, not by tests: GET /status returned 500.

/etc/secubox/waf is 0750 root:root while haproxy-routes.json inside it is 0644
— the file is world-readable, the directory is not traversable by the secubox
user the service runs as. Path.exists() raises PermissionError in that case, and
it sat OUTSIDE the try that already guarded read_text.

This is the fifth instance of the same class on this branch. The previous four
were 'unknown must not become a definite answer'; this one is its twin:
unknown must not become a crash either. An unreadable routes file now yields
None (undeterminable) as intended, so /status degrades to 'exposure unknown'
instead of 500ing the whole panel.

The directory permissions are the WAF module's business and are left alone —
this module's job is to survive them.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 13:40:21 +02:00
78f5e80747 feat(profiles): webui surface — own-socket API + hybrid-dark panel
Phase 1 stays read-only for actuation: no apply endpoint anywhere. Adds
api/web.py (FastAPI, JWT-gated) exposing status/profiles/pins/diff plus the
two writes Phase 1 allows — profile membership and pins — both atomic
(temp file + os.replace) and limited to /etc/secubox/profiles/*.toml.

A pin that would switch a protected module (auth/aggregator/core/nginx/
firewall/profiles) off is refused with HTTP 409 independently of
diff.plan_changes, before anything touches disk — proven load-bearing by
temporarily removing the guard and watching the test fail, then restoring
it and watching it pass.

Runs on its own service + socket (/run/secubox/profiles.sock), never via
the aggregator. Probe results that couldn't be determined surface as
"unknown", never silently downgraded to "off" (observe.is_on() collapses
None to False by design for diff's actionable decision, which would
misrepresent an indeterminate probe here).

www/profiles/index.html follows WEBUI-PANEL-GUIDELINES.md (hybrid-dark,
shared sidebar, data-* delegation, esc() on all injected values) with
stat grid, per-category/runtime/exposure aggregates, per-module pin/
membership controls, and a diff view explicitly labelled not-applied.

94/94 tests pass (73 pre-existing + 21 new, packages/secubox-profiles/tests).
2026-07-17 13:35:44 +02:00
2dff4868fe Merge feat/profiles-phase1: module inventory, profiler and diff (read-only)
Some checks are pending
License Headers / check (push) Waiting to run
Phase 1 of the profile system: flat-file manifests, a scan profiler that derives
them from reality and measures cost, pure state resolution, an ordered diff, and
the secubox-profilectl CLI. apply deliberately does not exist — it arrives in
Phase 3 with 4R snapshots, sequential application and audit; a test locks its
absence.

73 tests. Six review rounds caught five defects the tests could not: vacuous
negative tests short-circuited by an earlier guard, a failed probe fabricating a
definite False (three separate instances), a taxonomy guard defeated by a string
collision, a bypassable protected core, and an unbuildable package.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 13:19:01 +02:00
fc28ab7678 fix(profiles): unbuildable package, rc collision on scan, data-driven protection
C1: drop debian/compat (kept debhelper-compat (= 13) Build-Depends) — the
package was the only one in the repo carrying both, which debhelper refuses
to build.

C2: cli._run() now returns rc=None for "did not execute" (OSError/timeout),
mirroring observe._run_cmd's contract. _cmd_scan checks rc for both
systemctl list-unit-files and lxc-ls and aborts rather than silently
deriving LXC modules as runtime="native" from empty output; a non-root
invocation (rc=0, empty output) is refused explicitly since it is
indistinguishable from "no containers" by rc alone.

C3: PROTECTED_IDS moves from scan.py to manifest.py (a schema property, not
a profiler detail) and load_manifest() now forces protected=True for those
ids regardless of what a shipped manifest declares, closing the gap where a
sloppy module package could silently unprotect auth/aggregator/core/etc.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 13:14:02 +02:00
310f4713f5 Merge fix/users-secret-leak: stop leaking password hashes + TOTP secrets (#870)
Some checks are pending
License Headers / check (push) Waiting to run
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:59:25 +02:00
6f8a04fcad fix(users): stop leaking password hashes and TOTP secrets to any client
GET /api/v1/users/users, /user/{username} and /export returned raw records
from users.json, which holds the argon2id password hash, the TOTP *secret*,
and the argon2id hashes of the TOTP backup codes.

All three routes are gated by `require_jwt` — ANY authenticated account, not
just an admin — so the lowest-privilege user could harvest every other
account's credentials. The TOTP secret alone lets an attacker mint valid
6-digit codes forever: a complete MFA bypass. This is a privilege-escalation
vector on a product whose own charter forbids secrets in clear.

/export was the worst: its comment said "Remove sensitive data" while dropping
only `provision_results`. A comment asserting a guarantee the code does not
provide is worse than no comment — it stops reviewers from looking.

Add redact_user() as the single place that decides what a client may see, and
route all three through it. `totp` is kept but reduced to its non-secret
metadata, and its PRESENCE is preserved: the webui shows the MFA badge on a
truthiness test (`u.totp_enabled || u.totp || u.mfa_enabled`), so dropping the
key would blank the badge and always emitting one would light it up for users
who never enrolled.

It lives in its own module rather than main.py because main.py reads the config
at import, so importing it requires read access to /etc/secubox/secubox.conf —
a security rule must stay testable without privileges.

Verified live on gk2 across all three routes: no argon2 hash, no TOTP secret,
no backup codes; totp_enabled still correct per account (admin true, gk2 and
operator false). Mutation-checked: restoring the passthrough fails 5 of the 9
tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:58:38 +02:00
041b2d63ed fix(profiles): stop scan from silently downgrading exposure on unreadable routes
_cmd_scan collapsed load_routes()'s None (routes file present but
unreadable/corrupt, indeterminate) into the same set() as a genuinely
absent routes file, so a corrupt WAF routes file silently derived
every routed module's exposure down from public to lan/internal with
no operator feedback — and since a written manifest is authoritative,
the degraded value would stick until a manual --force rescan. scan now
warns on stderr in the indeterminate case only, naming the consequence
and the remedy, and stays silent when routes are genuinely absent.
_cmd_status/_cmd_diff were already correct and are untouched. Also
drops the unused pins_file binding in _cmd_status.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:49:38 +02:00
536eac0977 feat(profiles): CLI secubox-profilectl (scan/status/diff) + paquet
Phase 1 en lecture seule : apply n'existe pas, un test le verrouille.
Vérifié sur gk2 : scan découvre les modules et status affiche taxonomie +
coût RSS, sans qu'aucun changement de service ne soit attribuable à ces
commandes (fluctuation ±1 confirmée comme bruit préexistant des timers
oneshot du board, pas causée par cet outil).

Fix additif : load_routes() peut renvoyer None (fichier de routes corrompu),
scan._cmd_scan retombe sur set() plutôt que de planter dans discover().

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:43:20 +02:00
775175f0dc fix(profiles): stop menu.d mesh from colliding with deployment mesh
_category() let menu.d's UI catch-all "mesh" (declared by media modules
like peertube/lyrion) pass through as deployment category "mesh" simply
because the token matched — corrupting per-category stats. Replace the
membership check with an explicit MENU_CATEGORY_MAP, defaulting unknown
tokens to the neutral "infra" instead of a confidently wrong category.

Also make _toml_str escape control characters (\n \t \r) per TOML basic-
string rules — an unescaped literal newline in a field value previously
emitted a file tomllib itself could not reparse. Add roundtrip tests for
quote/backslash and control-character values, and replace a vacuous
"category in the whole enum" assertion with the exact expected value.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:29:33 +02:00
ec589adfca feat(profiles): profiler scan — dérive les manifestes du réel
134 manifestes ne s'écrivent pas à la main : on les dérive des units, LXC,
routes WAF et menu.d, puis l'opérateur corrige — et scan n'écrase plus.
Émetteur TOML écrit à la main (tomllib est en lecture seule), validé par
aller-retour avec le loader.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:19:16 +02:00
cb691e3577 feat(profiles): diff ordonné désiré vs réel
Ordre = décision de sûreté : stops avant starts (la box a ~2 Go libres, un pic
d'allumage la tuerait), stops par priorité croissante, starts par décroissante.
Un pin 'off' sur un module protégé est refusé, pas averti.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:11:45 +02:00
aa430739e6 fix(profiles): stop fabricating False in observe() systemd/routes branches
_run_cmd returned rc=1 both when systemctl genuinely answered "disabled"
and when the command failed to execute (OSError/timeout) — the latter
then read as a fabricated `enabled=False`/`active=False`, which a later
phase would treat as "off" and wrongly start. _run_cmd now returns
rc=None as the not-executed sentinel, and observe() maps it to None
instead of False for both enabled and active.

Likewise load_routes() folded an unreadable/corrupt routes file into the
same "no routes" set() as a genuinely absent file, producing a fabricated
portal_routed=False. It now returns None when the file exists but can't
be read/parsed, and observe() propagates that as portal_routed=None.

Also restores the `m: Manifest` type annotation on observe() that had
been dropped.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 12:04:55 +02:00
e0106674be feat(profiles): sondes d'état réel en lecture seule
L'inconnu reste None, jamais False : lxc-info échoue depuis le contexte non
privilégié de l'API (lxc_state='absent' alors que le service répond), et un
False inventé produirait une fausse décision d'allumage.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:56:13 +02:00
ea2f622503 feat(profiles): résolution de l'état désiré (protected > pin > profil > off)
Profils exhaustifs + pins persistants. resolve() est pure : c'est la règle la
plus critique du système, elle doit être testable sans board.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:51:20 +02:00
06b896c26c fix(profiles): make manifest negative tests reach their validators; validate needs/protected types
Six negative tests wrote fixtures to bad.toml while the TOML body still
declared id="lyrion", so they all raised on the id-mismatch check before
ever reaching the enum, lxc-requirement, or priority-bounds validator they
claimed to exercise. Fixed by writing to lyrion.toml so the intended check
is the one actually reached. Also fixed the runtime enum test's replace()
which searched for one space but MINIMAL uses two (column alignment),
making the substitution a silent no-op.

Added strict type validation for needs (must be list[str], not a bare
string that tuple() would explode into single chars) and protected (must
be bool, not a string coerced by truthiness) — protected gates whether a
module may ever be shut down, so silent coercion here is a live risk.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:46:58 +02:00
e53b049b6c feat(profiles): schéma et chargement des manifestes module
Valide strictement : un manifeste mal typé deviendrait une décision
d'extinction erronée en Phase 3.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:39:28 +02:00
722b5c1b03 docs(plan): profils & modules — Phase 1 (lecture seule)
6 tâches TDD : manifestes, résolution d'état, sondes, diff ordonné, profiler
scan, CLI + paquet. 55 tests.

Phase 1 ne peut rien allumer ni éteindre — un test verrouille l'absence de la
commande apply, et observe.py n'exécute que des commandes de lecture.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:36:40 +02:00
26eea724e1 docs(spec): conception du système de profils & activation de modules
Bascule de contexte au quotidien (média / sécurité / démo) : profils exhaustifs
+ pins persistants, toggle individuel, taxonomie des modules, dépendances
minimisées, priorités présentées.

Architecture retenue : manifestes plats + moteur de réconciliation. Les .target
systemd sont écartées : LXC, routes WAF et menus ne sont pas des units, et une
target n'exprime pas 'éteint'.

Le design est contraint par le réel mesuré sur gk2 : 187 units (118 actives),
load 5.4 sur 4 cœurs, ~2 Go libres, 24 LXC, 134 menu.d, et 80 units en
Requires=secubox-core (un oneshot mkdir) — d'où application séquentielle,
extinction avant allumage, et conversion Requires -> Wants en prérequis.

Noyau protégé non négociable : un profil ne doit pas pouvoir éteindre ce qui
permet de le rallumer.

Métriques mesh explicitement hors périmètre (spec séparé).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:29:04 +02:00
c2c1d0e008 docs: APK rebuilt + republished (SW v11, peertube, upload, 401 re-login)
Some checks are pending
License Headers / check (push) Waiting to run
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:12:06 +02:00
c9f2164c26 build(companion): commit the Capacitor Android scaffold + lockfile
The android/ project backing `npm run apk` was never tracked, so the APK build
was only reproducible on the machine that happened to have it.

The gradle wrapper is pinned to gradle-8.9-bin (the default 8.7-all is what
broke the build); the URL is the public distribution, and the tree carries no
machine-specific paths, so it builds anywhere.

.gitignore is the standard Capacitor/Android template — no APKs, build/,
.gradle/, local.properties, keystores, or the copied web assets under
app/src/main/assets/public (those are generated from www/ by cap sync, and
committing them would fork the app code).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:08:42 +02:00
7e46e323bf Merge feat/companion-peertube: PeerTube module in the Companion (#869)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:00:12 +02:00
06cea1ffb0 feat(companion): add PeerTube module
Video instance in the Companion: overview (state, version, counts, storage,
transcoding queue), video library, channels, and federation peers.

Every route was probed live and every shape read from
packages/secubox-peertube/api/main.py — none guessed. Verified against the real
instance: 42 videos, 3 channels, 17G storage.

Two box-side facts shape the view:
- `lxc_state` reads "absent" on this box while PeerTube serves fine (lxc-info
  cannot see an unprivileged container from the API's context). The box's own
  is_running() trusts http_reachable(), so the module does too — reporting
  "down" off lxc_state would be a lie the backend itself doesn't tell.
- The list routes answer {key:[...], total, error?} and SET `error` instead of
  failing when the instance is unreachable, so `error` is surfaced rather than
  rendered as a misleading "empty".

Each section degrades independently — one failing route cannot blank the
module. Storage rows with empty values are skipped (the live `videos` stat is
""). An idle transcoding queue says so explicitly rather than showing nothing.

Registered next to podcasteur (both MIND media modules). sw.js v10 -> v11.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:59:48 +02:00
28c4fbde84 Merge feat/sessions-companion-emoji: session auditing + Companion auth/system/metrics + billets emoji hashtags (#868)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:37:13 +02:00
1d0f578f1f docs: track session auditing + Companion auth/system/metrics + billets hashtags
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:36:29 +02:00
aef06b8b22 feat(companion): real auth/system modules, emoji metrics, 401 re-login
The auth and system modules were 34-line stubs guessing routes — auth called
/api/v1/auth/users, which 404s; identities actually live under /api/v1/users.
Both are rebuilt against routes read from the handler source, not guessed.

- core/metrics.js: emoji/severity gauges + human formatters. Thresholds are
  PER KIND because a disk at 94% and a cpu at 94% carry different risk and must
  not look alike — gk2's disk (94.5%) reads 🔴 while its cpu (10%) reads 🟢.
  Plain language over field names ("Memory 75% — 5.5 GB of 7.7 GB used").
- modules/auth: /api/v1/users/{users,sessions,roles,groups}; sessions rendered
  prominently (who / IP / device / age / current), which is the point of the
  session work landing alongside it.
- modules/system: /status, /metrics, /resources, /health_score, /network,
  /security — each section degrades independently so one failing route cannot
  blank the module.
- billets: tag chip bar + per-billet emoji chips, quick-view filtering, and
  short resumes instead of full bodies; list now uses the authoring endpoint,
  so Edit/Delete address real ids and drafts are visible.
- 401 re-login: box tokens live 24h, and the Companion seals one at pairing and
  reuses it — so a day later every authed call 401'd with no way out but
  unpairing ("Failed: unauthorized" in billets and podcaster). api.js now asks
  the app to re-authenticate and REPLAYS the request once; single-flight, so a
  screenful of concurrent 401s yields one prompt, and an in-flight write (a
  billet just typed) survives instead of being lost.
- sw.js v9→v10, metrics.js added to the offline shell.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:35:37 +02:00
f9302df075 feat(billets): emoji hashtags, quick views, and feed resumes
Authors already typed #hashtags in their bodies (37 of the 46 billets on gk2
carry one), so tags are EXTRACTED from the body rather than added via a picker:
tagging costs the author nothing and applies retroactively.

- migration 0004: tag(slug,emoji,label) + billet_tag, indexed on tag_slug.
  Extracted into rows rather than re-parsed per request, so filtering hits an
  index instead of LIKE-scanning every body. slug is accent-folded + lowercased
  (#Réseau == #reseau); emoji is STORED so a later curation change never
  silently restyles an already-published billet.
- services/tags.py: curated map + extraction. The lookbehind keeps a URL anchor
  (page#anchor) from becoming a tag; unknown tags fall back to a neutral badge
  rather than being rejected. Map covers this box's real content (podcast/media)
  as well as the security/box vocabulary; genuinely ambiguous topics are left on
  the default rather than editorialised.
- quick views: ?tag=slug on / and /feed.json, plus /tags.json and
  /admin/api/tags for the chip bar. Uses EXISTS, not a JOIN, so keyset paging
  cannot duplicate a row per matching tag; the pager carries the active tag.
- feed resumes: long billets show a short excerpt + "Lire la suite"; short ones
  render whole. `is_long` is measured on the markdown-collapsed text, so a short
  billet padded with link syntax is not "resumed" into a copy of itself.
- feed.json gains `summary` + `tags` (both JSON Feed 1.1 standard) with the
  emoji in the spec-legal `_secubox` extension; optional keys are omitted, not
  nulled.
- GET /admin/api/billets: the authoring list. The public feed cannot serve it —
  its item id is a permalink URL (so edit/delete could not address a row, which
  is why they 404'd) and it hides drafts, which an authoring client must see.
- manage.py backfill-tags: idempotent; also re-applies the curated map to
  existing tags as an explicit operator step.

Verified on gk2: backfill tagged 37/46 billets; ?tag=podcast→37, jfk→4, nope→0;
a draft's tag correctly stays out of the public chip bar; delete cascades.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:35:24 +02:00
b50387a06b fix(auth): record client IP + user-agent on the MFA login paths
/login/mfa and /totp/confirm both hardcoded "ip": "" and omitted user_agent
entirely, while only the password path captured them. admin is forced-TOTP,
so EVERY real admin login took the MFA path — every session row landed
unauditable (who logged in, from where, with what), and the users webui
sessions tab rendered blanks for data it was already asking for.

Factor the extraction into _client_meta(request) (X-Forwarded-For first: nginx
and HAProxy front every login, so request.client.host is only ever the proxy)
and use it on all three paths.

Verified by driving the real /login/mfa handler in-process with
SECUBOX_AUTH_SESSIONS pointed at a temp file: the session row now records
ip=192.168.1.77 (the first XFF hop, not the proxy) and the full user-agent.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:35:07 +02:00
bf97dc94c7 fix(companion): SW network-first bypasses HTTP cache (cache: reload)
Some checks are pending
License Headers / check (push) Waiting to run
A plain fetch(req) in the network-first handler still consults the browser
HTTP cache, which heuristically caches module JS (no Cache-Control on .js).
That let an old view.js survive a deploy — the recurring 'still broken after
hard refresh'. Refetch with cache:'reload' so every online load pulls fresh
app code; offline still falls back to the Cache API. Bump v7 -> v8.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:56:04 +02:00
ce31b59915 Merge fix/companion-billets-feed: billets JWT admin + Companion authoring/upload
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:51:29 +02:00
9792e0ad81 docs: track billets JWT admin + Companion authoring/upload
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:49:15 +02:00
db7fc2449e feat(billets): JWT admin surface + Companion authoring & image upload
Add packages/secubox-billets/api/routes/jwt_admin.py — a JWT-authed JSON
admin surface (secubox_core.auth.require_jwt: Bearer OR secubox_session
cookie) that reuses the same repo/media layer as the session HTML admin.
Endpoints: POST/PUT/DELETE /admin/api/billets, POST
/admin/api/billets/{id}/media (multipart, EXIF-strip via services.media),
GET /admin/api/comments, approve/delete comment moderation. No-op if
secubox_core is absent (isolated unit tests, session admin unaffected).

Wire it into api/main.py (register_jwt_admin after register_admin).

postinst: drop a sorted-last zz-secubox-system.pth into the venv so the
isolated billets venv can import the .deb-managed secubox_core without
shadowing its own fastapi/pydantic wheels.

Companion billets module: point EP map at /feed.json + /admin/api/*, add
an image file input to the editor and upload it after the billet is
created (POST /admin/api/billets/{id}/media, multipart). Fixes the "New
billet missing upload" gap. Bump service worker v6 -> v7.

Verified live on gk2: create -> feed, valid PNG upload -> {url,thumb},
delete — all 200.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:48:12 +02:00
b928ab71f7 fix(companion): bump SW to v6 so the billets feed.json fix reaches stale clients
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:27:26 +02:00
6afd155d69 fix(companion): billets module reads the real feed (/feed.json), not /feed
The billets module called `/feed` (404) — billets serves a JSON Feed at
/feed.json — and read `published_at` where JSON Feed uses `date_published`;
status_endpoint was /health (billets serves /healthz). Fixed all three, so the
#/m/billets list loads (30 items) instead of {"detail":"Not Found"}. Writes
(new billet, comment moderation) remain unavailable — billets exposes only GET
routes (its admin is the session-based web UI); those tabs already degrade
gracefully. A JWT write API for billets is a separate feature.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:24:20 +02:00
60f059ac62 docs: WIP/HISTORY for 2026-07-17 WAF (webui/autoban/analysis) + Nextcloud rework (PR #866, #867)
Some checks are pending
License Headers / check (push) Waiting to run
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 06:30:27 +02:00
CyberMind
116aca49ab
Merge pull request #867 from CyberMind-FR/fix/nextcloud-webui-status
fix(nextcloud): working status probe + cyan webui reskin
2026-07-17 06:29:01 +02:00
CyberMind
1c9171a4f0
Merge pull request #866 from CyberMind-FR/fix/waf-autoban-cscli-bridge
fix(waf): sbxwaf auto-bans reach the firewall (cscli bridge + dedup)
2026-07-17 06:28:56 +02:00
7e926eeb7f fix(nextcloud): working status probe + cyan webui reskin
The API runs unprivileged (secubox) but probed the container via lxc-info/occ
WITHOUT sudo and with lxc_path=/srv/lxc (container is /data/lxc) → status always
'Stopped', cloud.local domain, empty version/users/disk (and a 500 once the path
was corrected, from a PermissionError on /data/lxc). Now: privilege-free TCP port
probe for liveness, all container ops via 'sudo nextcloudctl' (the sole sudoers
surface, incl. its occ passthrough), a daemon-thread 60s cache for the slow occ
fields (version/users/storage) so /status stays fast, real data_path, and a
public web_url from the domain. Webui restyled to the /certs/ cyan hybrid-skin
(361 lines, emoji cards, live pulse, 30s refresh); every endpoint preserved.

Needs (board drift): NoNewPrivileges=false drop-in (sudo was blocked) and
[nextcloud] domain=nc.gk2.secubox.in in secubox.conf.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 05:00:37 +02:00
a9ab57b5e7 fix(waf): make sbxwaf auto-bans reach the firewall (cscli bridge + dedup)
The CrowdSec bridge was silently OFF: the unit passes --crowdsec-url but no
--crowdsec-jwt-file, so the wiring hit the 'disabled' branch and srv.crowdsec
stayed nil → Report() was never called. 198K threats detected, ~0 auto-bans
enforced (the 36k-hit offender was never nft-dropped). The LAPI /v1/alerts path
was doubly broken anyway: JWTs expire hourly and our alert schema 500s on this
build.

Add CscliReporter — the proven path the dashboard's manual ban uses
('cscli decisions add', which creates a real bouncer-enforced nft drop). It
engages when --crowdsec-url is set without a JWT file (new --crowdsec-cscli
flag, default 'cscli'). Per-IP 5-min dedup collapses the storm from the
graduated ban firing Report on every banned request (a rapid attacker otherwise
spawned dozens of concurrent cscli procs that contended until killed); on cscli
failure the IP is un-recorded so the next hit retries. Verified live: 5 honeypot
hits from a test IP → exactly 1 decision, no storm.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 16:28:34 +02:00
8617ba2083 feat(waf): rework sbxwaf dashboard to cyan hybrid-skin look
Some checks are pending
License Headers / check (push) Waiting to run
Restyle-only rework of the WAF webui to the guidelined /certs/ cyan hybrid-skin
(shared hybrid-skin.css, body.hybrid-dark, emoji cards, live pulse dot, reactive
30s/10s refresh), dropping the old crt-light/crt-engine/sidebar-light includes.
Every loader/action + its endpoint/method/payload preserved verbatim
(alerts, bans, categories, stats, status, category toggle, bans/history, visits,
mode, bans/clear, export, ban, unban). 1354 → 1028 lines.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 15:42:15 +02:00
3e2a359410 docs: WIP/HISTORY for 2026-07-16 mail/auth/dpi session (PR #865, #862-#864)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 15:36:20 +02:00
CyberMind
1269fd303c
Merge pull request #865 from CyberMind-FR/fix/mail-dpi-auth-webui-20260716
fix: mail (auth/domain/status/reskin/password) + DPI (exfil/regen) + shared auth cookie-fallback + openclaw CT
2026-07-16 15:34:43 +02:00
a96c3b1c4a fix(mail): /user/password writes the real dovecot passwd-file (admin reset was a no-op)
The endpoint shelled to 'mailctl user passwd', which writes a host-side copy the
LXC never reads, so admin password resets silently didn't take (login kept
failing). Now generates the SHA512-CRYPT hash via the container's doveadm and
updates DATA_PATH/config/users directly (the bind-mounted file dovecot reads),
normalising any legacy 2-field line to the full 8-field record.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 12:00:41 +02:00
0ebce730d6 fix(openclaw): certSpotter fallback for CT lookup + whois registrable domain
crt.sh is frequently down (502/404 HTML) and its raw error leaked into scan
results. Now queries certSpotter first (reliable JSON), falls back to crt.sh,
then a clean message. whois now resolves the registrable domain for sub-domains.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00
8a01d5f631 feat(dpi): /exfil serves cumulative R3 devices + engine liveness; webui regen on real engine
/exfil read the idle live-window state.json (empty when the wg-toolbox tunnel is
idle) so the dashboard showed no devices; now serves the cumulative rollup (real
devices) + overlays live active_flows + an engine-liveness block. Service control
repointed from dormant netifyd to the real Go collector (secubox-dpi-flowcap) via
a scoped sudoers grant (postinst). Webui regenerated in the cyan hybrid-skin look,
framed on the R3 engine, netifyd/mirred/block-rules cruft removed; the XSS-safe
media-buffer rendering is preserved verbatim.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00
83c8094141 fix(mail): live TCP-probe status + certs-cyan webui reskin
Status/components/ports now use direct TCP probes instead of lxc-info (which
read the wrong lxc_path and reported everything Stopped); storage reads vmail.
Webui rebuilt in the /certs/ hybrid-skin look: emoji stat-cards, 15s live
pulse, External-mailboxes tab, scanline engine dropped; all endpoints preserved.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00
5c46ad864a fix(auth): require_jwt tries Bearer then cookie so a stale token can't shadow a live session
A present-but-stale localStorage Bearer was used exclusively, ignoring a valid
secubox_session cookie, so every webui 401'd into a /login.html loop. Now both
sources are tried (Bearer first, then cookie); 401 only if neither validates.
Strictly more permissive — no regression for working Bearer clients.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00