Commit Graph

2793 Commits

Author SHA1 Message Date
a670cda9e6 docs: openclaw dedicated-LXC OSINT+scanner design 2026-07-09 05:08:47 +02:00
87b2ab9dd8 feat(threat-analyst): remove Endpoint status diagnostic panel from webui 2026-07-09 04:58:28 +02:00
d041f12a05 docs: c3box kernel migration design (6.12.85 + one-shot boot guard)
Some checks are pending
License Headers / check (push) Waiting to run
2026-07-08 13:15:31 +02:00
372dc677c3 docs: plan addendum — real /storage via nextcloudctl (fold b715 #429) (ref #429) 2026-07-08 07:22:10 +02:00
5096b5f015 docs: implementation plan — Nextcloud dashboard real data + manageable users (ref #429) 2026-07-08 07:20:17 +02:00
c1885c9646 docs: spec — Nextcloud dashboard real data + manageable users (ref #429) 2026-07-08 07:13:39 +02:00
CyberMind
4f11e03285
feat(sentinel): plumb JA4 into the pipeline — makes non_browser_ja live (ref #826) (#831)
Some checks failed
License Headers / check (push) Has been cancelled
* feat(sbxmitm): plumb SNI-independent client-stack JA4 into the Sentinel FlowMeta — makes non_browser_ja live (ref #826)

* docs(sbxmitm): clarify ja4stack is ad-hoc (not spec JA4), self-consistent with capture (ref #826)

* release: toolbox-ng 0.1.34 — JA4 plumbing (ref #826)

* fix(sbxmitm): exclude GREASE from ja4stack (RFC 8701) so the fingerprint is stable per client (ref #826)

* release: toolbox-ng 0.1.35 — GREASE-stable ja4stack (ref #826)

* feat(sentinel): browser-ja4.txt as an /etc conffile (operator-maintained, upgrade-safe) + capture-based population docs (ref #826)

* release: toolbox-ng 0.1.36 — browser-ja4 /etc conffile (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 12:50:55 +02:00
CyberMind
fe4b12bf64
fix(sentinel): correct MVT/Pegasus feed URL + env-gated JA4 capture infra (ref #826) (#830)
Some checks are pending
License Headers / check (push) Waiting to run
* feat(sentinel): env-gated JA4 capture (bootstrap browser-ja4 from live traffic) + fix MVT feed URL (Amnesty repo) (ref #826)

* release: toolbox-ng 0.1.33 — MVT feed URL fix + JA4 capture (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 12:28:39 +02:00
CyberMind
72c57ccfac
feat(sentinel): report-only surfacing of non-spyware feed IOCs (botnet_c2/malware) — ref #826 (#829)
* feat(sentinel): report-only IOC reporter — surface non-spyware feed detections (botnet_c2/malware) as observed, never blocked (ref #826)

* release: toolbox-ng 0.1.32 — report-only feed surfacing (ref #826)

* docs(sentinel): iocreport MatchIP uses Host (destination), not ClientIP (source) — false-positive vector + IP test (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 12:14:37 +02:00
CyberMind
f19d566793
fix(sentinel): C2 auto-learn false positive — require a strong signal (ref #826) (#828)
Some checks are pending
License Headers / check (push) Waiting to run
* release: toolbox-ng 0.1.30 + toolbox 2.8.2 — Sentinel C2 auto-learning (ref #826)

* fix(sentinel): C2 false-positive — require strong signal (dga/non-browser), gate non_browser_ja on configured browser set (ref #826)

* release: toolbox-ng 0.1.31 — C2 false-positive fix (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 08:13:48 +02:00
0008c89e58 docs: track Sentinel engine + activation + 3 surfaces + C2 auto-learning (#821 #823-#828) 2026-07-07 08:09:59 +02:00
1b42406a33 release: toolbox-ng 0.1.30 + toolbox 2.8.2 — Sentinel C2 auto-learning (ref #826) 2026-07-07 07:46:51 +02:00
CyberMind
62e8631f2b
feat(sentinel): C2/botnet auto-learning — high-precision, report-only (Closes #826) (#827)
* docs: spec — Sentinel C2/botnet auto-learning (high-precision, report-only) (ref #823)

* docs: implementation plan — Sentinel C2/botnet auto-learning (ref #823)

* feat(sentinel): C2 autolearn false-positive allowlist gate (ref #826)

* fix(sentinel): serialize c2allow Add + robust atomicWriteFile (relative-path + temp cleanup) (ref #826)

* feat(sentinel): C2 autolearn corroborating signals (rare/non-browser-JA/DGA) (ref #826)

* feat(sentinel): C2 autolearn candidate lifecycle + persistence (ref #826)

* fix(sentinel): c2cand deep-copy snapshots (persist/snapshot/record race) + evict order + device cap (ref #826)

* feat(sentinel): C2 autolearn orchestrator — learned set + report-only verdicts (ref #826)

* feat(sentinel): wire C2 learner into daemon + /c2 endpoints + seeded allowlist (ref #826)

* fix(sentinel): make /c2/allow writable in packaged deploy (RW path + ownership) + sanitize Add (ref #826)

POST /c2/allow could never write in production: sbx-sentinel.service runs
User=secubox-toolbox under ProtectSystem=strict + ReadOnlyPaths=/etc/secubox,
and the seeded c2-allow.txt ships root:root. Fix both halves: nest
ReadWritePaths=/etc/secubox/sentinel under the existing ReadOnlyPaths=/etc/secubox
in the unit, and chown/chmod 0750 the sentinel/ subdir + file (never the shared
/etc/secubox parent) in postinst's configure branch, fail-safe.

Also: fix the /c2/allow doc comment (form/query only, not JSON — r.FormValue
never parses a body), and sanitize C2Allow.Add against newline injection now
that host is network-reachable.

* feat(toolbox): WebUI C2 appris sub-view + Ignorer allowlist proxy (ref #826)

* fix(toolbox): C2 view XSS (data-host not onclick) + normalize candidate signal maps (ref #826)

* fix(sentinel): bound c2 beaconing/learned maps + drop promoted from candidates + reject bare-TLD allow (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 07:46:19 +02:00
CyberMind
916cb91b72
feat(toolbox): activate Sentinel + surface compromise detections (WebUI tab · kbin report · PDF) — ref #823 (#825)
Some checks are pending
License Headers / check (push) Waiting to run
* feat(toolbox): arcade-HUD transparency banner redesign + responsive close-X fix

Redesign both MITM transparency-banner renderers into a C3BOX arcade-HUD
"scoreboard" skin and fix the phone bug where the close X dropped off-screen.

Structural responsive fix (both renderers): three flex clusters — LEFT rank
pinned (flex:0 0 auto), MIDDLE evidence flex:1 1 auto;min-width:0;overflow-x:auto
(scrolls instead of pushing), RIGHT report+close pinned (flex:0 0 auto) so the
close X is always visible; box-sizing:border-box added.

bundle.py _BANNER_CORE (primary R3 renderer): still built entirely via the mk()
DOM API (no innerHTML — Trusted-Types/strict-CSP invariant kept). Tier-accent
palette (r0 gold..r4 pink) drives border/glow/rank chip/level switch; evidence +
Tor/Ad-Guard toggles + live sbx-trk/sbx-ck counts become neon pills; one scoped
prefers-reduced-motion <style> for sheen/hover. All ids, toggles, csp proof,
un-hideable styles, SPA re-assert preserved.

inject_banner.py _banner_html_dynamic (secondary addon renderer): same look +
three-cluster fix; keeps grade shield + geo pills, _LEVEL_THEME, _GUARD, NCR
ascii-encodability, gondwana-mitm-banner id, JS-path close moved to right cluster.

Tests updated to the new structure; invariants still asserted (no innerHTML,
ids, toggles, ascii-encodability, dismiss control in right cluster).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(nginx): revalidate luci-static assets (Cache-Control no-cache) so webui updates aren't cached stale

* docs: spec — activate Sentinel + surface on WebUI tab, kbin report, PDF (ref #823)

* docs: implementation plan — Sentinel activation + 3 ToolBoX surfaces (ref #823)

* feat(sentinel): add mac filter to /verdicts status endpoint (ref #823)

* feat(toolbox): sentinel_link — fail-safe daemon fetch + compromise assess (ref #823)

* fix(toolbox): harden sentinel_link int coercions to never raise (ref #823)

* feat(toolbox): /admin/sentinel/{stats,verdicts} fleet proxy routes (ref #823)

* fix(toolbox): guard /admin/sentinel/stats against non-numeric daemon values (ref #823)

* feat(toolbox): WebUI Sentinelle fleet tab (evaluation + detections) (ref #823)

* feat(toolbox): fold per-device Sentinel detections into build_report_data (ref #823)

* feat(toolbox): kbin report per-device Compromission tab (ref #823)

* feat(toolbox): wire per-device Sentinel data into live HTML report render (ref #823)

* fix(toolbox): report-live tab must render when report var is absent (ref #823)

* feat(toolbox): PDF + text-fallback Sentinel compromise section (ref #823)

* docs: GPT demo prompt for kbin Sentinel augmentation (ref #823)

* fix(toolbox): sentinel never-raise on non-dict daemon rows + guard ts render (ref #823)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:39:42 +02:00
CyberMind
afd8f6eb83
feat(sentinel): sbxmitm Sentinel — inline+async exploit/malware/spyware detection engine (#823) (#824)
Some checks are pending
License Headers / check (push) Waiting to run
* docs(sentinel): design — inline IOC gate + async Go analyzer + spyware packs (ref #823)

* docs(sentinel): implementation plan — 12 tasks (ref #823)

* feat(sentinel): IOC model + IOCSet matchers (ref #823)

* feat(sentinel): pack loader + live-overlay merge + hot-reload (ref #823)

Add internal/sentinel/pack.go: Pack (JSON base/overlay content bundle),
LoadBasePack, MergePacks (base + N overlays, override by Type+Value via
IOCSet's per-type maps), and Loader (NewLoader/Set/MaybeReload/YaraRules).

Loader mirrors cmd/sbxmitm/policy.go's LoadPolicy/maybeReload mtime-based
hot-reload pattern, adapted for a directory of a DYNAMIC *.json file set
(glob-based) rather than policy.go's fixed Target list. Fail-safe: a
corrupt overlay pack is skipped+logged, never wipes the base or panics;
MaybeReload is a cheap stat-only no-op when nothing on disk changed;
Set()/YaraRules() read via atomic.Pointer so a reload swap never torn-reads
a concurrent hot-path lookup.

Adds JSON tags to Task 1's IOC struct (type/value/class/severity/source/
action) so packs round-trip through encoding/json.

* fix(sentinel): pack loader — fail-closed on base-wipe + throttle MaybeReload + url_regex override + yara dedup (ref #823)

Review found 1 Critical + 2 Important + 1 Minor in Task 2 (pack loader):

- Critical: a vanished/degraded base pack directory (deleted dir, mount
  blip, mid-dpkg-upgrade window) could silently empty the live detection
  set on the next MaybeReload. loaderState now tracks baseCount; a reload
  that would drop base IOCs from >0 to 0 is refused (prior state kept,
  warning logged) instead of swapped in.
- Important: MaybeReload() is called per-flow by Task 3's inline gate: add
  a 5s default throttle (mirrors cmd/sbxmitm/policy.go's reloadThrottle)
  so a hot-path call is a pure no-op until the window elapses.
- Important: url_regex IOCs previously only appended, so a live overlay
  could never override a base pack's URL rule (base always matched
  first). Add urlIndex (pattern -> slice index) so re-adding the same
  pattern replaces the entry in place, keeping deterministic match order.
- Minor: YaraRules() now dedups identical rule bodies across base+overlay
  packs instead of emitting duplicates.

go test ./internal/sentinel/: 21/21 PASS. go vet/gofmt/go build clean.

* feat(sentinel): inline gate matcher (fail-open, highest-severity) (ref #823)

* feat(sentinel): bbolt verdict store (pure-Go, WAL-free, TTL prune) (ref #823)

* feat(sentinel): action scorer — heuristic/low-confidence forced report-only (ref #823)

* feat(sentinel): bounded fire-and-forget mirror channel (drop-with-count) (ref #823)

* fix(sentinel): mirror Emit always copies body — no aliasing of caller's hot-path buffer (ref #823)

* feat(sbxmitm): inline Sentinel gate — neutralize high-confidence, mirror rest, fail-open (ref #823)

Wire internal/sentinel's inline IOC gate into the shared mitmPipeline response
path (covers both the transparent R3 and CONNECT accept paths without drift):
build a FlowMeta from Host/URL/ClientIP/MacHash after up.Do, call the gate, and
on a HIGH-CONFIDENCE known-infra hit neutralize inline — block/sinkhole serve a
Sentinel block page instead of the upstream response, strip serves the response
with an emptied body. Everything else (miss, heuristic, low-confidence, report)
is mirrored bounded fire-and-forget to the async analyzer and proceeds unchanged.

Fail-open everywhere: SENTINEL_ENABLED unset/false, a pack-load failure, a nil
hook, or a gate panic all degrade to a transparent passthrough — a Sentinel
setup problem can never break browsing. With the engine off, behavior is
byte-identical to today. Env: SENTINEL_ENABLED / SENTINEL_PACK_DIR /
SENTINEL_OVERLAY_DIR / SENTINEL_MIRROR_SOCK.

Hot path (BenchmarkSentinelInspectMiss, -benchmem): 101.0 ns/op, 0 B/op,
0 allocs/op — one throttled gate Match + a non-blocking mirror Emit; no YARA or
heavy analysis inline.

go test ./cmd/sbxmitm/... -race → ok (all pre-existing + 7 new green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(sentinel): sbx-sentinel async daemon scaffold + store wiring (ref #823)

* feat(sentinel): YARA engine (cgo, build-tagged) + no-cgo stub (ref #823)

Add internal/sentinel/yara.go (cgo && yara) wrapping
github.com/hillu/go-yara/v4 for compiled-rule body scanning, and
yara_stub.go (!yara) as the no-op default: NewYaraEngine always
succeeds, Scan/Analyze always return nil. Both expose identical
signatures (YaraEngine, NewYaraEngine, Scan, Analyze, Close) so
sbx-sentinel/sbxmitm can wire a YaraEngine unconditionally.

yara_test.go (untagged) proves the default build works with YARA
disabled. yara_real_test.go (yara tag) compiles a trivial EICAR rule
and exercises the real libyara-backed engine — split into its own
file since Go build constraints are file-scoped and can't mix a
tagged and untagged test in one file.

go-yara is now an indirect go.mod requirement and vendored, but is
only reachable via the yara build tag: default `go build ./...` and
`CGO_ENABLED=0 go build ./...` both stay clean, no libyara/cgo
required. `-tags yara` needs libyara-dev + a C toolchain (documented
in yara.go's header).

* fix(sentinel): YARA verdict confidence 90 (>= threshold) so a confirmed malware match actually strips, not report-only (ref #823)

* feat(sentinel): behavioral engine — beaconing + zero-click heuristics (report-only) (ref #823)

* fix(sentinel): tighten one-time-link heuristic — exclude file-extension + base64 shapes (cut zero-click FPs) (ref #823)

* feat(sentinel): spyware indicator engine + commercial-spyware base pack (MVT/CitizenLab) (ref #823)

* fix(sentinel): base-pack declared-block IOCs severity>=85 so they actually block + general guard test (ref #823)

Three shipped base-pack IOCs declared action:"block" but severity:80,
below scorer.go's HighConfidenceThreshold=85 — FinalizeAction silently
downgrades any Confidence below that threshold to report-only, so these
entries never actually blocked despite being declared as known-bad:

- packs/base/malware.json: ip 198.51.100.23 (class malware)
- packs/base/malware.json: cert_sha1 ...01234567 (class trojan)
- packs/base/botnet.json: ja3 deadbeef... (class botnet_c2)

Bumped all three to severity 90 so they survive FinalizeAction and
actually block, matching every other declared-block entry in the base
packs.

Added TestBasePackDeclaredBlockIsHighSeverity, a general guard over
every base pack file/entry: any IOC with an auto-neutralize action
(block/strip/sinkhole) must carry Severity >= HighConfidenceThreshold.
This generalizes the narrower TestBasePackKnownInfraIsHighSeverity
(spyware classes only) so a future pack entry in any class can't ship
a "block" that silently never blocks. Confirmed the new test fails red
against the pre-fix JSON (all 3 offenders reported) and passes green
after the severity bump.

* feat(sentinel): proposal/solution reporter — RenderReport(Verdict) (ref #823)

* feat(sentinel): wire real analyzers into sbx-sentinel + read-only status HTTP surface (ref #823)

* feat(sentinel): live threat-feed → overlay pack fetcher (fail-safe, atomic) (ref #823)

* feat(sentinel): live feed overlay + reporter + daemon analyzer wiring + packaging (ref #823)

* fix(sentinel): feeds fetcher — add curl --fail so HTTP 4xx/5xx is treated as a fetch failure (ref #823)

* fix(sbxmitm): drop mis-wired ClientIP from inline Sentinel FlowMeta (source IP must not match destination-C2 IP IOCs) (ref #823)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:53:21 +02:00
CyberMind
e79dfc8368
feat(frigate): Frigate NVR Foundation — podman-in-LXC on amd64 + /api/v1/frigate shim (#821) (#822)
Some checks are pending
License Headers / check (push) Waiting to run
* docs(frigate): Foundation design — LXC podman on amd64 + /api/v1/frigate shim + WAF-fronted (ref #821)

* docs(frigate): Foundation implementation plan — 7 tasks (ref #821)

* feat(frigate): package scaffold — control/rules/changelog + placeholder (ref #821)

* feat(frigate): idempotent LXC + podman provisioning (mirror photoprism) (ref #821)

* feat(frigate): config example + reconcile example path/db-path/image-override (ref #821)

Task 3: conf/frigate.config.yml.example — OpenVINO CPU detector, go2rtc
demo source (boots with no real camera), record/snapshots retention,
mqtt.enabled: false, commented real-camera block.

Task 2 review reconciliations:
- debian/rules now ships the example to the canonical path
  /usr/share/secubox/frigate/frigate.config.yml.example, matching what
  install-lxc.sh's install_config() actually reads (was wrongly shipped
  to /etc/secubox/frigate.config.yml.example).
- config example adds database.path: /media/frigate/db/frigate.db so
  Frigate's SQLite DB lands on /data/frigate (bind-mounted), not the
  Frigate default /config/frigate.db.
- install-lxc.sh templates the installed frigate.service unit's image
  tag from $IMAGE via sed after install -D (idempotent, fail-safe via
  grep -qF check), so SECUBOX_FRIGATE_IMAGE overrides actually take
  effect instead of silently running the hardcoded :0.14.1. Also drops
  the unused `openssl` from require_cmds (dead dep from photoprism).

* feat(frigate): /api/v1/frigate shim — status/cameras/events/storage/stats, JWT, double-cached, fail-safe (ref #821)

* fix(frigate): shim robustness — ImportError-only JWT fallback, cache /storage, bound /events, single /api/stats fetch, + fail-safe tests (ref #821)

* feat(frigate): host shim service + disk-pressure guard + frigatectl (ref #821)

* fix(frigate): diskguard survives missing/unmounted /data (was silent exit 1) + regression test (ref #821)

* feat(frigate): nginx shim route + menu.d + documented WAF exposure & sidebar stats (ref #821)

* feat(frigate): postinst/prerm (provision LXC, data-preserving) + build verification (ref #821)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-06 11:53:20 +02:00
CyberMind
fecd233d88
feat: Media Buffer Phase 2 — HLS reassembly via read-time URL join (#815)
Some checks failed
License Headers / check (push) Has been cancelled
* feat(sbxmitm): capture HLS segments as kind=segment (Phase 2, ref #812)

Add segmentKind(path, ctype) to classify HLS/DASH segment chunks (.ts,
.m4s, .m4v paths or video/mp2t, video/iso.segment content types), and
give it precedence over mediaKind in Capture so a .ts served as
video/mp2t is tagged kind="segment" instead of "video". Whole-file
.mp4/video/* stays "video" and .m3u8/.mpd manifests stay "manifest".
IsMedia now also matches segmentKind so ambiguous-ctype segments (e.g.
.m4s over application/octet-stream) are still captured.

* feat(core): HLS media-playlist parser + segment-URI rewriter (Phase 2, ref #812)

* feat(dpi): HLS manifest replay — rewrite segment URIs to captured segments (Phase 2, ref #812)

media_replay() branches on record kind: a captured HLS manifest is parsed
with secubox_core.hls and its segment URIs are rewritten to the matching
captured kind=="segment" records' replay URLs (read-time join by absolute
URL, scoped to the same mac_hash+host). Master/ABR and AES-128 encrypted
playlists are detected and returned raw with X-SecuBox-Media:
unsupported-variant instead of a broken rewrite. Every other kind keeps the
unchanged Phase-1 FileResponse path; any manifest parse/read error falls
back to the raw FileResponse (never a 500).

* fix(dpi): log HLS manifest/segment/rewrite truncation + pin manifest replay fail-safe (Phase 2 review, ref #812)

* feat(dpi): Media tab — hide HLS segments, manifest playback/download + unsupported-variant note (Phase 2, ref #812)

* test(#812): HLS reassembly end-to-end + changelog 0.1.28 (Phase 2)

Cross-layer integration test seeding a temp media-buffer exactly as the Go
tee + janitor produce on disk (metatag JSONL + per-session object files),
then driving the real DPI media_replay handler: manifest rewrite joins each
segment URI to its captured segment by resolved URL, every rewritten URL's
id replays to byte-identical captured bytes, and a janitor-style append-only
eviction flip leaves the evicted segment unrewritten (410 on direct replay)
while its siblings keep replaying. This is the whole-branch-review-flagged
integration coverage Phase 1 was missing.

Bumps secubox-toolbox-ng changelog to 0.1.28-1~bookworm1 with the Phase 2
HLS reassembly summary (segment capture, manifest rewrite,
unsupported-variant detection for master/ABR + AES + DASH).

* chore(dpi): changelog 1.2.0 — media buffer API + HLS reassembly (#812 Phase 1+2)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-04 18:17:40 +02:00
CyberMind
418126c541
feat: SecuBox Media Buffer Phase 1 — dashcam capture + replay + metatag lifecycle (#814)
* docs(media-buffer): design spec — dashcam capture + replay + metatag lifecycle (ref #812)

* docs(media-buffer): phase-1 implementation plan (capture+janitor+API+DPI tab) (ref #812)

* feat(sbxmitm): media buffer store + metatag writer (ref #812)

* fix(sbxmitm): media-buffer review — 4096 atomic-append cap + *string buffer_ref (ref #812)

Task 1 review (Important): mediaBufferLineMax 8192→4096 to match mediacatch.go's
PIPE_BUF atomic-append guarantee across 4 workers; BufferRef string→*string so the
janitor (Task 3) can flip it to JSON null on eviction.

* feat(sbxmitm): tee media up/download bodies into the buffer (ref #812)

Wire the R4 media buffer tees into mitmPipeline: the download hook wraps
resp.Body and the upload path wraps req.Body via teeReadCloser, capturing
whole-file media into the rolling /data buffer. Add the mbuf field + flags
(--media-buffer default OFF, --media-buffer-root, --media-buffer-per-object).

Design upgrade over the plan sketch: ObjectWriter.Write is now NON-BLOCKING.
Instead of io.TeeReader driving a synchronous os.File.Write on the client read
path (which would add latency / could stall large media on ARM eMMC), Write
copies each chunk into a bounded channel and returns len(p),nil immediately; a
background goroutine started at Capture drains the channel to disk. A full
channel drops the chunk and flags truncated — never blocks, never errors the
proxied flow (design 4.2/7). Close signals the drainer, waits for it to flush
queued chunks + close the file, then appends the metatag; it is idempotent,
deadlock-free (sole closer of ch) and safe if Write is never called or races
Close.

* fix(sbxmitm): media download tee — defer closure so w.Close runs (ref #812)

defer resp.Body.Close() bound the ORIGINAL upstream body at the defer
statement, before the media-buffer tee reassigned resp.Body — so the
tee's Close() (and w.Close, which flushes the sink/metatag/drain
goroutine) never ran on any download. Wrap the close in a closure so
it reads resp.Body at return time instead.

Adds TestTeeDeferOrderingMatchesMainGo, which replicates main.go's
exact defer-then-reassign ordering and fails against the old plain
defer form (verified manually, then restored the fix).

Also: drop the dead write-only ObjectWriter.dropped field (truncated
already covers it), document Capture's reserved contentLen param, and
note that ObjectWriter.Close is allowed to block (post-stream
finalisation), unlike Write.

* feat(sbxmitm): media buffer janitor — time + LRU eviction, metatag survives (ref #812)

SweepOnce evicts a session's bytes (RemoveAll, idempotent) once
first_ts+retentionSecs elapses, then falls back to oldest-first LRU
eviction if the buffer root (media bytes only, log excluded) still
exceeds sizeCeilBytes. Eviction never rewrites the append-only
metatag log in place — it appends a corrected line with the same id
(expired:true, buffer_ref:null); readers dedup by id, last line wins,
so the flip is durable and safe across the 4 concurrent sbxmitm
worker processes each running their own RunJanitor (idempotent
RemoveAll + harmless duplicate flip lines).

RunJanitor ticks SweepOnce every 30s (test-overridable period) until
its context is cancelled; wired at sbxmitm startup as a
process-lifetime goroutine when --media-buffer is enabled.

NewMediaBuffer's signature is unchanged; retentionSecs (1200),
sizeCeilBytes (24 GiB) and nowFn now default inside the constructor.

* feat(core): media-buffer metatag reader (dedup by id, fail-empty) (ref #812)

* test(core): media-buffer reader — tail-read truncation + malformed-field regressions (ref #812)

* chore(#812): keep SDD scratch reports out of the feature diff (restore to master)

* feat(dpi): media-buffer list/replay/thumb API (admin/owner, audited, 410-on-evict, traversal-safe) (ref #812)

* harden(dpi): media API — \Z-anchored id regex + role short-circuit security note (ref #812)

* feat(sbxmitm): media-buffer retention/size-ceil flags + /data tmpfiles + opt-in worker env toggle (ref #812)

- main.go: --media-buffer-retention / --media-buffer-size-ceil flags, applied
  onto the already-constructed mbuf's unexported retentionSecs/sizeCeilBytes
  fields (NewMediaBuffer signature unchanged).
- tmpfiles/zz-secubox-media-buffer.conf: pre-creates /data/secubox (0755) and
  /data/secubox/media-buffer (0750 secubox:secubox), installed via the
  package's existing manual tmpfiles.d mechanism in debian/rules.
- secubox-toolbox-ng-worker@.service: EnvironmentFile=-/etc/secubox/toolbox-ng.env
  + trailing $SBX_MEDIA_BUFFER_ARGS on ExecStart so an operator can opt in
  (SBX_MEDIA_BUFFER_ARGS=--media-buffer) without editing the shipped unit;
  default stays OFF. Added ReadWritePaths=/data/secubox/media-buffer.

* fix(sbxmitm): media-buffer dir owned by writer (secubox-toolbox) + setgid group secubox; changelog 0.1.27 (ref #812)

Task 6 review Critical: buffer dir was 0750 secubox:secubox but sbxmitm workers run
as secubox-toolbox (not in group secubox) → EACCES on every capture, silently no-op'd
(errors swallowed by design). Fix: 2750 secubox-toolbox:secubox — writer owns, dpi
reader (secubox) is group, setgid propagates group secubox to created objects so the
reader can read them. Important: add debian/changelog 0.1.27 stanza (version bump).

* feat(dpi): Media tab — live capture gallery with replay/download, escaped fields (ref #812)

Adds a Media Buffer card to the DPI dashboard (packages/secubox-dpi/www/dpi/index.html)
that lists sbxmitm's live media-buffer captures via GET /api/v1/dpi/media/buffer.

- Auth: dedicated apiAuthed() reads the canonical `sbx_token` localStorage key
  and sends it as a Bearer token (media/buffer requires JWT, unlike the rest
  of this page's endpoints which currently ride the SSO-lite session cookie);
  401 redirects to /login.html, matching sibling dashboards (e.g. crowdsec).
- Security: host/url/mac_hash are attacker-influenceable (captured live
  traffic). Every card is built via createElement + textContent (or the
  `.title` IDL property, never HTML-parsed) — no innerHTML string
  concatenation of these fields — so a captured value like
  `<img onerror=...>` renders as inert text. Replay/download URLs are built
  with encodeURIComponent(id) even though the id is hex-validated server-side.
- UI: kind emoji (video/audio/manifest/file), short device id (shortDev,
  reused from the existing #785 helpers), ⬆/⬇ direction, human size
  (formatBytes) and age (agoStr) per card; expired captures grey out and show
  "métatag seul (expiré)" with no action links.
- Refresh: wired into the existing refreshAll() plus its own ~15s interval.

Verified with a Playwright harness stubbing fetch (incl. an XSS payload in
host/url) against a local copy of the page: no console/page errors, XSS did
not fire, no <img>/<script> element materialized from the captured strings,
expired-card gating confirmed.

* fix(sbxmitm): media buffer — upload tee needs a real body, download tee 200-only (whole-branch review); drop dead item.ts (ref #812)

* docs(media-buffer): Phase 2 plan — HLS reassembly via read-time URL join (ref #812)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-04 18:15:58 +02:00
CyberMind
670bf0779b
fix(dpi): unblock the shared aggregator loop (14 handlers async def → def, #807) (#808)
secubox-dpi is aggregator-mounted (nginx /api/v1/dpi/ → aggregator.sock; dpi in
aggregator.toml), so its route handlers run in-process on the shared aggregator
uvicorn loop (~110 modules, single worker). 14 handlers were `async def` but call
blocking sync code (subprocess.run for status/logs/interface_list/tc_status/
restart/start/stop/remove_mirred; .read_text on 100–213KB caches for exfil_state/
exfil_history/realtime/block_rules/add_block_rule/delete_block_rule) with no await
— each blocked the whole loop while it ran → board-wide 502/504 (same class as the
peertube #804 and toolbox PDF #785 loop-blocks).

None use await and none are awaited internally, so plain `def` is a safe, clean
conversion: FastAPI runs def path operations in a threadpool, fully off the loop.

Board dpi/api/main.py == git source (no drift): the anti-pattern is in source, so
this is the source fix. test_media_types.py green.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-04 18:15:38 +02:00
CyberMind
e4a9c8e452
fix(peertube): stop webui login loop — getToken() reads sbx_token (#810) (#811)
The PeerTube dashboard bounced to /login.html?redirect=/peertube/ and looped after
a successful login. getToken() read localStorage 'jwt_token' || 'token' — keys the
canonical login.html never writes. login.html (and every other module webui: waf,
soc, crowdsec, metrics, shared sidebar.js) uses 'sbx_token'. So PeerTube always sent
an empty Authorization → API 401 → api()'s 401 handler redirected to login → login
stored sbx_token → back to /peertube/ → getToken() read jwt_token (empty) → 401 → loop.

Pre-existing divergence (present in the May-28 backup); not caused by #798. Same class
as the cookies webui drift (#749). Fix: read sbx_token first, keep the legacy keys as
fallback. Applied live on the board to unblock.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-04 18:15:25 +02:00
CyberMind
4a6b853785
fix(peertube): drain admin-ops results to a separate dir (fixes start-limit loop, #798) (#804)
* fix(peertube): drain admin-ops results to a separate dir (fixes start-limit loop, ref #798)

The root peertube-ops.path watches OPS_DIR with DirectoryNotEmpty=. process-ops
wrote each <id>.result.json back into OPS_DIR, so the directory was never empty
after draining a request → the .path re-triggered peertube-ops.service in a tight
loop until systemd killed it with start-limit-hit, after which the mechanism died
after the first op.

Write results to a dedicated /run/secubox/peertube/results/ instead; process-ops
rm's the request, leaving OPS_DIR empty so the watcher goes idle. The API's
_read_op_result now polls RESULTS_DIR. tmpfiles + postinst create the dir
(0750 secubox:secubox); peertubectl honors SECUBOX_PEERTUBE_RESULTS_DIR.

test_process_ops.sh asserts results land in results/ and OPS_DIR is empty after
draining (regression guard); test_ops_api monkeypatches both dirs.

Verified live on gk2: two consecutive pings drain cleanly, NRestarts=0, no
start-limit-hit, results root:secubox 0640 (secubox-readable).

* fix(peertube): upgrade CWD must be peertube-latest/scripts + surface the error (ref #798)

The webui upgrade button failed with a generic "upgrade script failed" and no detail.
Two bugs in cmd_upgrade:

1. Wrong CWD. PeerTube's v8 shim peertube-latest/scripts/upgrade.sh execs
   `../dist/scripts/upgrade.sh` relative to CWD, and that dist script runs
   `node -e "require('js-yaml')…"` whose resolution is also CWD-relative. Running from
   `cd /var/www/peertube` made `../dist` → /var/www/dist (missing) → "cannot open
   ../dist/scripts/upgrade.sh"; a neutral CWD instead fails "Cannot find module
   'js-yaml'". Fix: cd /var/www/peertube/peertube-latest/scripts (../dist resolves,
   and require() walks up to peertube-latest/node_modules).

2. Error was discarded to /dev/null, so the result only said "upgrade script failed".
   Now capture to /run/secubox/peertube/upgrade-<id>.log and fold the last lines into
   the error detail.

Verified live: with the corrected CWD, 8.2.0 → 8.2.2 upgraded cleanly (download + deps
+ migrations), service restarted, /api/v1/config 200, serverVersion 8.2.2.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-04 18:15:21 +02:00
CyberMind
3802f67ea6
fix(sbxwaf): forward WebSocket upgrades (Zigbee dashboard wss:// broke, #796) (#805)
* fix(sbxwaf): forward WebSocket upgrades — don't clobber Connection: Upgrade (closes #796)

sbxwaf forced Connection: close on every upstream request (#496), which
overwrote Connection: Upgrade on WebSocket handshakes → httputil.ReverseProxy
stopped tunnelling the WS, forwarded a plain GET, and wss:// vhosts (zigbee, …)
got 404/1006. Guard the Connection rewrite with isWebSocketUpgrade() so upgrades
are tunnelled (still routed + inspected via the WAF, no bypass).

* fix(sbxwaf): statusRecorder must implement http.Hijacker for WS upgrades (#796)

The visit-stats wrapper (statusRecorder, #747) embeds http.ResponseWriter but
didn't expose Hijack, so with --visits-stats active ReverseProxy's WebSocket
upgrade failed the http.Hijacker assertion → 503. Forward Hijack to the
underlying writer. Test now enables visits so it exercises the wrapped path.

* fix(sbxwaf): cachingResponseWriter must forward Hijack + log upstream errors (#796)

Third WS-breaking layer: the media-cache tee wrapper (cachingResponseWriter)
also shadowed http.Hijacker → ReverseProxy's WS upgrade failed with 'can't
switch protocols using non-Hijacker ResponseWriter type *cachingResponseWriter'.
Forward Hijack to the underlying writer (hijacked conn is raw-tunnelled, nothing
cached). Also log upstream errors in the routes proxy ErrorHandler — they were
invisible before, which is what made this hard to diagnose.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-04 18:15:17 +02:00
a7d17eeb08 chore: #806 SDD complete — READY TO MERGE (marker) 2026-07-04 18:06:16 +02:00
c625715432 fix(toolbox): mesh-exclusion churn guard + never-raises + env overrides + fed-disabled enabled flag (ref #806) 2026-07-04 18:02:10 +02:00
d607691270 feat(toolbox): package mesh-exclusion publish+sync timers (ref #806) 2026-07-04 17:47:43 +02:00
ce178b9435 feat(toolbox): 🌐 mesh badge for federated exclusion entries (ref #806) 2026-07-04 17:43:10 +02:00
7672761d39 fix(toolbox): mesh-exclusion union tolerates malformed blob fields (ref #806)
The union_blobs function now guards against non-list field values (e.g. int,
bool, None) that could cause TypeError during iteration. Instead of assuming
a field is always a list, the function checks isinstance() before iterating.

Adds test_union_blobs_tolerates_malformed_fields() to verify that a valid
blob with malformed fields does not crash the entire federation sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 17:40:45 +02:00
6d886cb61c feat(toolbox): mesh-exclusion sync (pull+union+write fed files) (ref #806) 2026-07-04 17:35:58 +02:00
936e9e02a9 docs: record commit hash in task-2 fix report (ref #806) 2026-07-04 17:32:52 +02:00
b13314d7fc fix(toolbox): mesh-exclusion publish sends JWT bearer + fd-safe annuaire call (ref #806) 2026-07-04 17:32:33 +02:00
9be001eeac feat(toolbox): mesh-exclusion payload builder + publisher (ref #806) 2026-07-04 17:25:28 +02:00
5b63c4ba5b feat(toolbox-ng): sbxmitm reads federated exclusion lists + union disabled (ref #806) 2026-07-04 17:21:28 +02:00
4979631138 docs(plan): mesh-federate toolbox exclusion lists — 5-task TDD plan (#806)
Some checks are pending
License Headers / check (push) Waiting to run
2026-07-04 17:06:45 +02:00
25ce4693a2 docs(spec): mesh-federate toolbox exclusion lists (#806) 2026-07-04 17:01:16 +02:00
9acfe826e5 feat(toolbox): Filtres MITM per-item enable/disable + delete with real engine effect (#809)
Some checks are pending
License Headers / check (push) Waiting to run
Engine (sbxmitm): new operator-disabled set (mitm-filter-disabled.txt, hot-
reloaded) suppresses matching per-pattern in BOTH the bypass and splice paths —
so unchecking an entry in the webui actually stops the R3 engine applying it,
including package-seed entries. bypassEntry keeps the source pattern beside the
compiled regex; hostMatchesEnabled skips disabled splice suffixes. Tests:
TestDisabledSuppressesMatch (bypass + splice) + full suite green.

API: POST /admin/filter-control/toggle (add/remove from disabled) + /delete
(remove line from editable files; package seed → disable). /list rows gain
enabled + editable.

Webui: each Filtres MITM row gets a  checkbox (bound to toggle, greyed+struck
when disabled) + 🗑 delete (locked-tooltip for seeds). Live on gk2 (workers +
portal restarted, old binary at sbxmitm.bak.pre-809).
2026-07-04 16:03:36 +02:00
7c45bd2eaa fix(toolbox): #ads surfaces the DNS-layer blocking (the real ad-blocker)
Some checks are pending
License Headers / check (push) Waiting to run
'Pubs bloquées: 0' was misleading — ad-blocking moved to the DNS layer
(secubox-adblock-sync #740: Unbound sinkholes 757,946 ad/tracker domains via
always_nxdomain), so ads die BEFORE reaching the engine → the engine's 204
counter reads ~0. /admin/ad-stats now includes dns_sinkhole (from ad-guard
sinkhole-status.json) and the #ads tab leads with a '🛡️ Blocage DNS' card
(domains + rules + sources), relabels the engine card '204 moteur MITM'. Live gk2.
2026-07-04 15:35:04 +02:00
2f798859cc fix(toolbox-ng): ad-block wins over bypass-splice (#803 regression, #ads)
#803 folded the mitm-bypass match into shouldSplice (checked BEFORE ad-block),
so ad networks that also live in the bypass seed (adform/amazon-adsystem/
rubiconproject/smartadserver…) started SPLICING instead of being 204-blocked —
silently disabling ad-blocking for them. Move the bypass match OUT of
shouldSplice into Decide AFTER blockedByAd: explicit tls-splice → ad-block →
bypass-splice (cert-pinned apps only). Signal/banks still splice; ad nets in the
bypass list block again. New regression-guard case (adform.net→block). Live gk2.
2026-07-04 15:22:51 +02:00
b6572fa9da fix(toolbox): tmpfiles pins media-catch /run file to worker uid (#801)
Ships tmpfiles.d/secubox-toolbox.conf creating /run/secubox/media-catch.jsonl
0644 secubox-toolbox:secubox at boot (before services), so the R3 workers can
always append. Fixes the wrong-owner race where the file was pre-created as
secubox → workers' write-open failed → media-catch silently empty. Makes the
2026-07-04 live perms fix durable across reboots. Applied live on gk2.
2026-07-04 14:57:54 +02:00
06db2ff68e feat(toolbox): #ads dashboard surfaces detected candidates (#802)
ad_stats now returns total_candidates + top_candidates from ad_candidates
(observed ad/tracker hosts awaiting autolearn promotion to active 204-blocking).
The #ads tab gains a '🔎 Trackers/pubs détectés' card so the dashboard reflects
real detection even when total_blocked is 0 (nothing promoted yet) — was
showing all-zeros + empty tables despite live detection. Deployed to gk2.
2026-07-04 14:56:25 +02:00
85d1fd8bcc feat(toolbox-ng): sbxmitm reads the mitm-bypass list → cert-pinned apps splice (#803)
The R3 Go engine only honoured the TLS-splice list, so every cert-pinned app in
the mitm-bypass (ignore_hosts) list — Signal/WhatsApp/Telegram/Apple/banks,
incl. regex-wildcard patterns a suffix list can't express (ca-.*\.fr) — was
MITM'd and broke through the tunnel. Load the three bypass files
(seed/static/dynamic) as anchored case-insensitive regexes; a match in
shouldSplice() now returns splice (never-set still wins). Hot-reloaded on edit/
autolearn like the splice lists. This is the systemic fix behind the
Claude/Signal interim splice-learned merges. Live on gk2 (workers restarted);
full sbxmitm suite green + new TestBypassRegexSplices.
2026-07-04 14:53:06 +02:00
767e4391b8 feat(toolbox): wire Tor-egress host exemption into reconcile + timer (#799)
Some checks are pending
License Headers / check (push) Waiting to run
populate_exempt() now calls secubox-toolbox-tor-exempt-hosts (resolves the
splice/cert-pinned list → IPs → tor_exempt) so spliced pinned upstreams (Claude,
Signal, banks) egress DIRECT when Tor egress is armed — else the spliced
passthrough is torified and the pinned API breaks. Ships the resolver +
refresh timer (30min, for CDN IP rotation), enabled in postinst. Deployed live
to gk2 (Tor egress currently ON): exempted 14 pinned hosts / 28 IPs.
2026-07-04 14:16:04 +02:00
5cf1ed19d6 feat(toolbox): Filtres MITM shows the FULL exclusion set (bypass + splice), uncapped
- /admin/filter-control/list now also surfaces the TLS-splice lists
  (tls-splice-seed + splice-learned, inline-comment-stripped) tagged
  splice-seed/splice-learned, so every explicitly-spliced host is displayed +
  known (was: only the 3 bypass files → splice hosts invisible). 84 entries now.
- webui: removed the slice(0,5) cap → shows all with a scroll container + count
  header + 🔀 splice badges.
- add secubox-toolbox-tor-exempt-hosts resolver (#799, not yet wired/deployed).
2026-07-04 14:11:42 +02:00
1b22e1a25d docs: correct WIP note — 872be8c7 was a parallel-session commit, not a stray 2026-07-04 14:02:34 +02:00
872be8c77b feat(toolbox): splice autolearn mirrors into webui MITM filter list; rename WAF Filters → Filtres MITM
- secubox-toolbox-autolearn now mirrors each splice-learned host into
  mitm-bypass-dynamic.conf (the Filtres MITM '🔍 learned' source) as bypass
  regex, so autolearned splice hosts are visible + editable in the webui and
  honoured on the ignore_hosts path. Merge (never clobbers operator entries).
- secubox-mitmproxy filters.html: 'WAF Filters' → 'Filtres MITM' (title/h1/desc);
  API path /api/v1/mitmproxy/waf unchanged.
2026-07-04 14:01:54 +02:00
eddc1af372 docs: track Zigbee WS fix (#796/#805) + PeerTube ops loop fix (#798/#804) 2026-07-04 14:01:54 +02:00
CyberMind
10e1bc8ff4
PeerTube WebUI: admin reset-password + version check/upgrade (#800)
Some checks are pending
License Headers / check (push) Waiting to run
* docs(spec): PeerTube WebUI admin ops — reset-password + version check/upgrade (ref #798)

* docs(plan): PeerTube WebUI admin ops implementation plan (ref #798)

* feat(peertube): API ops spool plumbing + require_admin (ref #798)

* feat(peertube): spool→root ops mechanism (process-ops + path/service) (ref #798)

* feat(peertube): reset admin password button (lockout-safe, spool→root) (ref #798)

* fix(peertube): admin-secret chown to real secubox user + WebUI error field (ref #798)

* feat(peertube): version check (installed vs latest GitHub release) (ref #798)

* feat(peertube): one-click upgrade with pre-backup + rollback (spool→root) (ref #798)

* fix(peertube): require_admin store lookup + op-name align + result group-readable (whole-branch review, ref #798)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-04 10:12:08 +02:00
a3e19bba4e feat(vhost): redirect vhost support (redirect_to → 301)
Adds redirect_to to VHostCreate + generate_vhost_config: when set, the vhost is
a pure 301 redirect (no proxy, no exposure gate). create_vhost requires a
backend only for non-redirect vhosts. Covers the www.example.com → apex pattern
(deployed live for www.ganimed.fr). 18 vhost tests pass.
2026-07-04 09:45:52 +02:00
59225017c3 fix(toolbox): splice whole anthropic.com + claude.ai (Claude API through R3 tunnel)
Some checks are pending
License Headers / check (push) Waiting to run
Only the exact api.anthropic.com was spliced; Claude Code / the SDK also hit
s-cdn.anthropic.com + statsig.anthropic.com (seen MITM'd in ja4 logs), which
cert-pin → the API broke through the wg-toolbox tunnel. Splice the whole
anthropic.com domain (suffix covers all subdomains) + claude.ai. Live fix
applied to gk2 splice-learned.txt + workers restarted; this makes it durable
and fleet-wide.
2026-07-04 09:44:19 +02:00
94f3430a7d fix(exposure): gate /tor/add + emancipate; fail-open on undecodable list (ref #797)
Some checks are pending
License Headers / check (push) Waiting to run
Review found the cert-pin gate only covered POST /exposure — /tor/add and
/emancipate (which routes through tor_add) could still mint an onion for a
pinned host. Gate tor_add on the ORIGINAL service (before dot-stripping) so all
three Tor-creation paths are covered. Also: _lines() now fails open on
UnicodeDecodeError (a non-UTF-8 byte no longer 500s the gate), and an
mtime-keyed cache avoids recompiling the bypass regexes on every request.

Deferred (tracked in #797): periodic re-validation of already-active onions when
a host is autolearned into exclusion later; the live Go sbxmitm engine consumes
only the splice list (bypass regex half is webui/autolearn config).
2026-07-04 08:18:51 +02:00
c13878ecbc feat(exposure): Tor refuses cert-pinned/MITM-bypassed hosts (ref #797)
The per-vhost Tor channel now consults the toolbox's own MITM-exclusion lists
(ignore_hosts bypass regex + TLS-splice suffix, both operator-editable in the
Filtres MITM webui and autolearned) and hard-refuses (400) a hidden service for
any matching host — never anonymously re-expose an endpoint clients pin. reach
(lan/wan) still applies. Toolbox webui shows a Tor-exclu badge.
2026-07-04 08:09:37 +02:00