mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 07:26:08 +00:00
Compare commits
No commits in common. "dc6505a2f2c5123bce3903af715280ac4ce8a2f3" and "46dfd781d3588fa12fd5aa56e5a17164b9a8b83d" have entirely different histories.
dc6505a2f2
...
46dfd781d3
|
|
@ -3,41 +3,6 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2026-06-13 — Browser extension : emancipate cartographie live (ref #532)
|
|
||||||
|
|
||||||
Nouveau client `clients/webext-toolbox/` (MV3 Firefox `.xpi` + Chromium),
|
|
||||||
sœur de l'app Android. Surface la cartographie sociale R3 dans le
|
|
||||||
navigateur : badge live des traceurs + popup (4 tuiles + mini Round-Eye
|
|
||||||
graph SVG sans dépendance + top-traceurs taggés CDN/anti-bot/opérateur +
|
|
||||||
actions cartographie/PDF/RGPD-wipe). Parle uniquement à la cabine via R3
|
|
||||||
(pas de CORS backend grâce à host_permissions).
|
|
||||||
|
|
||||||
`secubox-toolbox 2.6.14` : `GET /wg/toolbox.xpi` (local sinon 302 →
|
|
||||||
release), bouton onboard, helper `secubox-toolbox-fetch-xpi`, postinst
|
|
||||||
dir. CI `build-webext.yml` (`web-ext lint` + build, release asset sur tag
|
|
||||||
`webext-v*`). Suivi : signature AMO, SSE `/social/live`, icône PNG
|
|
||||||
Chromium, Poke/Emancipate (#525).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-06-13 — Android ToolBox app : serve + root-mode silent onboarding (ref #531/#536/#538)
|
|
||||||
|
|
||||||
App compagnon Android one-tap R3 (`clients/android-toolbox/`, Kotlin + Compose).
|
|
||||||
|
|
||||||
- **#531** — scaffold Gradle/Compose + CI `build-android-apk.yml` (debug APK
|
|
||||||
artifact, release asset sur tag `android-v*`). CI green.
|
|
||||||
- **#536** — `GET /wg/toolbox.apk` (build local sinon 302 → release GitHub) +
|
|
||||||
bouton onboard kbin + helper `secubox-toolbox-fetch-apk`.
|
|
||||||
- **#538** (PR #539) — root-mode silent onboarding : install CA système
|
|
||||||
(bind-mount cacerts + APEX conscrypt, SELinux ctx, `subject_hash_old`
|
|
||||||
pur Kotlin) + WireGuard natif noyau + vérif R3 auto, gated derrière le tap
|
|
||||||
`⚡ Installation automatique (root)`. Fallback handoff app WireGuard.
|
|
||||||
Fichiers `RootShell.kt`, `RootOnboard.kt`, step `RootAuto`. CI APK build
|
|
||||||
green (code compile).
|
|
||||||
- Suivi : release signing (keystore CI) pour empreinte publiée stable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2026-06-11 — Phase 12.C + Phase 13 protection enforcement plane COMPLETE (ref #518-#528)
|
## 2026-06-11 — Phase 12.C + Phase 13 protection enforcement plane COMPLETE (ref #518-#528)
|
||||||
|
|
||||||
`secubox-toolbox 2.6.6 → 2.6.11`, tags v2.13.16 → v2.13.19.
|
`secubox-toolbox 2.6.6 → 2.6.11`, tags v2.13.16 → v2.13.19.
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,10 @@
|
||||||
# TODO — SecuBox-DEB Backlog
|
# TODO — SecuBox-DEB Backlog
|
||||||
*Mis à jour : 2026-06-13*
|
*Mis à jour : 2026-06-11*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔥 P0 — Immediate (in flight)
|
## 🔥 P0 — Immediate (in flight)
|
||||||
|
|
||||||
### ToolBox clients (`clients/`)
|
|
||||||
|
|
||||||
- [x] **#531 Android scaffold + CI** — Gradle/Compose one-tap onboarding,
|
|
||||||
debug APK via `build-android-apk.yml`. CI green.
|
|
||||||
- [x] **#536 serve APK from toolbox** — `GET /wg/toolbox.apk` + onboard button +
|
|
||||||
`secubox-toolbox-fetch-apk` helper.
|
|
||||||
- [x] **#538 Android root-mode silent** (PR #539) — system CA install + native
|
|
||||||
kernel WireGuard + auto R3 verify, gated behind explicit root tap.
|
|
||||||
- [x] **#532 browser extension** (`clients/webext-toolbox/`) — MV3 Firefox
|
|
||||||
`.xpi`/Chromium; live tracker badge + popup mini Round-Eye graph over
|
|
||||||
`/social/*`; `GET /wg/toolbox.xpi` + fetch helper + `build-webext.yml`.
|
|
||||||
- [x] **#532 release** — tag `webext-v0.1.1` published the `.xpi`
|
|
||||||
(downloadable, verified 200). `make_latest:false` + tag-pinned URL so it
|
|
||||||
doesn't steal "Latest" from the Android APK release.
|
|
||||||
- [ ] **release signing** — Android keystore + AMO `.xpi` signing secrets in CI
|
|
||||||
for stable published fingerprints (currently unsigned sideload).
|
|
||||||
- [ ] **#532 follow-ups** — optional `GET /social/live/{token}` SSE (replace the
|
|
||||||
client-side poll) ; Poke/Emancipate per-site control once #525 (deception)
|
|
||||||
ships ; Chromium PNG icon rasterisation for the Web Store.
|
|
||||||
|
|
||||||
### Phase 13 — Protection enforcement plane (#519) — ✅ COMPLETE
|
### Phase 13 — Protection enforcement plane (#519) — ✅ COMPLETE
|
||||||
|
|
||||||
- [x] **13.A spine** (#521, `2.6.8`, v2.13.17) — nft blacklist set + forward-drop
|
- [x] **13.A spine** (#521, `2.6.8`, v2.13.17) — nft blacklist set + forward-drop
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,5 @@
|
||||||
# WIP — Work In Progress
|
# WIP — Work In Progress
|
||||||
*Mis à jour : 2026-06-13*
|
*Mis à jour : 2026-06-11*
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 2026-06-13 : Browser extension — emancipate cartographie live (#532)
|
|
||||||
|
|
||||||
Extension navigateur (`clients/webext-toolbox/`, MV3 Firefox `.xpi` +
|
|
||||||
Chromium) sœur de l'app Android. Sort la *cartographie sociale* R3 dans
|
|
||||||
le navigateur : badge live des traceurs + popup.
|
|
||||||
|
|
||||||
- **Extension** : `manifest.json` (MV3, background `service_worker` +
|
|
||||||
`scripts` pour FF115+/Chromium), `api.js` (client `/wg/r3-check`,
|
|
||||||
`/social/me` → token, `/social/graph/{token}`, `/social/wipe`),
|
|
||||||
`background.js` (badge = total_trackers, re-pair silencieux si token
|
|
||||||
expiré, couleur escalade gold→anti-bot→opérateur), popup (4 tuiles
|
|
||||||
stats + **mini Round-Eye graph SVG sans dépendance** + top-traceurs
|
|
||||||
taggés CDN/anti-bot/opérateur + actions cartographie/PDF/RGPD-wipe),
|
|
||||||
options (hôte/fenêtre/token manuel). Pas de CORS backend nécessaire
|
|
||||||
(host_permissions). Validé : JSON+JS+SVG OK, `.xpi` build 11.8 KB.
|
|
||||||
- **Serve depuis la toolbox** (`2.6.14`) : `GET /wg/toolbox.xpi` (local
|
|
||||||
sinon 302 → release), bouton `🧩 Extension navigateur` sur les 2
|
|
||||||
panneaux onboard, helper `secubox-toolbox-fetch-xpi`, postinst dir.
|
|
||||||
- **CI** : `build-webext.yml` — `web-ext lint` (0 erreur, 2 warnings
|
|
||||||
bénins) + build, artifact, release asset sur tag `webext-v*`.
|
|
||||||
- **Release** (PR #540 + #541, mergées) : tag `webext-v0.1.1` poussé →
|
|
||||||
CI a publié `secubox-toolbox-webext.xpi` (téléchargeable, vérifié 200).
|
|
||||||
`make_latest:false` + URL **tag-pinned** dans `/wg/toolbox.xpi` +
|
|
||||||
`secubox-toolbox-fetch-xpi` pour ne pas voler le pointeur "Latest" à la
|
|
||||||
release APK Android (dont l'endpoint résout via `/releases/latest/...`).
|
|
||||||
→ bumper le tag dans la constante + le helper à chaque `webext-v*`.
|
|
||||||
- **Reste à faire** : signature AMO (`.xpi` non signé = sideload/dev) ;
|
|
||||||
endpoint SSE `/social/live/{token}` optionnel ; icône PNG Chromium ;
|
|
||||||
contrôle Poke/Emancipate par-site quand #525 (déception) arrive ;
|
|
||||||
déployer `secubox-toolbox 2.6.14` sur la board pour activer le bouton.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 2026-06-13 : Android ToolBox app — serve + root-mode silent onboarding (#531/#536/#538)
|
|
||||||
|
|
||||||
App compagnon Android **one-tap R3** pour la cabine VILLAGE3B
|
|
||||||
(`clients/android-toolbox/`, `in.secubox.toolbox`, Kotlin + Compose).
|
|
||||||
|
|
||||||
- **#531 — scaffold + CI** : projet Gradle/Compose (5-step stepper
|
|
||||||
Discover→InstallCa→ImportProfile→Verify→Done), client `HttpURLConnection`,
|
|
||||||
workflow `build-android-apk.yml` (debug APK artifact, release asset sur
|
|
||||||
tag `android-v*`). CI **GREEN**.
|
|
||||||
- **#536 — serve depuis la toolbox** : endpoint `GET /wg/toolbox.apk`
|
|
||||||
(sert le build local `/var/lib/secubox/toolbox/android/`, sinon 302 →
|
|
||||||
release GitHub) + bouton *📱 Installer l'app ToolBoX (1-tap)* dans les
|
|
||||||
panneaux onboard kbin + helper `secubox-toolbox-fetch-apk`. Vérifié :
|
|
||||||
200 `application/vnd.android.package-archive`, 14.8 MB.
|
|
||||||
- **#538 — root-mode silent onboarding** (PR #539, branche poussée) :
|
|
||||||
bouton *⚡ Installation automatique (root)* sur devices rootés →
|
|
||||||
install CA dans le magasin **système** (bind-mount cacerts + APEX
|
|
||||||
conscrypt, SELinux ctx, `subject_hash_old` en Kotlin pur) + tunnel
|
|
||||||
WireGuard **natif noyau** (`ip link add … type wireguard` + `wg set`) +
|
|
||||||
vérif R3 auto. Fallback handoff app WireGuard si noyau sans WG. Toutes
|
|
||||||
les actions root gated derrière le tap explicite. Nouveaux fichiers
|
|
||||||
`RootShell.kt`, `RootOnboard.kt`, step `RootAuto` (log streamé).
|
|
||||||
- **Reste à faire** : release signing (keystore secret CI) pour une
|
|
||||||
empreinte publiée stable — actuellement debug-signé (sideload).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
63
.github/workflows/build-webext.yml
vendored
63
.github/workflows/build-webext.yml
vendored
|
|
@ -1,63 +0,0 @@
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Build the SecuBox ToolBoX browser extension (#532).
|
|
||||||
# Plain JS/HTML/CSS — no bundler. web-ext lints + packages the .xpi.
|
|
||||||
# Produces an unsigned .xpi artifact; release signing (AMO) is a
|
|
||||||
# follow-up (needs AMO API credentials as secrets).
|
|
||||||
name: build-webext
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
paths: [ "clients/webext-toolbox/**", ".github/workflows/build-webext.yml" ]
|
|
||||||
tags: [ "webext-v*" ]
|
|
||||||
pull_request:
|
|
||||||
paths: [ "clients/webext-toolbox/**" ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write # needed to attach the .xpi to a release on tags
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: clients/webext-toolbox
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Lint (web-ext)
|
|
||||||
run: npx --yes web-ext lint --source-dir . --self-hosted --ignore-files build.sh README.md
|
|
||||||
|
|
||||||
- name: Build .xpi (web-ext)
|
|
||||||
run: |
|
|
||||||
npx --yes web-ext build --source-dir . \
|
|
||||||
--artifacts-dir web-ext-artifacts --overwrite-dest \
|
|
||||||
--ignore-files build.sh README.md \
|
|
||||||
--filename "secubox-toolbox-webext.xpi"
|
|
||||||
|
|
||||||
- name: Upload .xpi artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: secubox-toolbox-webext
|
|
||||||
path: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
# On webext-v* tags, publish the .xpi as a release asset under the
|
|
||||||
# stable name the toolbox fetch helper + /wg/toolbox.xpi expect.
|
|
||||||
# make_latest:false so this client release does NOT steal the
|
|
||||||
# "latest" pointer from the Android APK release (which the APK
|
|
||||||
# endpoint resolves via /releases/latest/download/…). The xpi
|
|
||||||
# endpoint/fetcher therefore use a tag-pinned download URL.
|
|
||||||
- name: Publish release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/webext-v')
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: clients/webext-toolbox/web-ext-artifacts/secubox-toolbox-webext.xpi
|
|
||||||
fail_on_unmatched_files: true
|
|
||||||
make_latest: false
|
|
||||||
|
|
@ -5,39 +5,13 @@ One-tap **R3 onboarding** for the VILLAGE3B cabine : install the CA,
|
||||||
import the WireGuard profile, verify the tunnel, then open the live
|
import the WireGuard profile, verify the tunnel, then open the live
|
||||||
*cartographie sociale*. Replaces the manual Android tutorial.
|
*cartographie sociale*. Replaces the manual Android tutorial.
|
||||||
|
|
||||||
## Flow (manual path)
|
## Flow
|
||||||
1. **Discover** — scan the kbin QR or type the booth host (`kbin.gk2.secubox.in`).
|
1. **Discover** — scan the kbin QR or type the booth host (`kbin.gk2.secubox.in`).
|
||||||
2. **Install CA** — downloads `/wg/ca.crt`, launches the Android cert-install intent (`KeyChain.createInstallIntent`).
|
2. **Install CA** — downloads `/wg/ca.crt`, launches the Android cert-install intent (`KeyChain.createInstallIntent`).
|
||||||
3. **Import profile** — downloads `/wg/profile/new`, hands the `.conf` to the WireGuard app via `FileProvider` + `ACTION_VIEW`.
|
3. **Import profile** — downloads `/wg/profile/new`, hands the `.conf` to the WireGuard app via `FileProvider` + `ACTION_VIEW`.
|
||||||
4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓".
|
4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓".
|
||||||
5. **Live metrics** — opens `/social/me` (cartographie sociale).
|
5. **Live metrics** — opens `/social/me` (cartographie sociale).
|
||||||
|
|
||||||
## Root path — fully-automated silent onboarding (#538, #551)
|
|
||||||
When the device is **rooted**, the app runs the whole onboarding with **zero
|
|
||||||
taps**: on launch it auto-detects root and, if this cabine host hasn't been
|
|
||||||
onboarded yet, starts the silent sequence automatically (`RootAuto` step,
|
|
||||||
streaming log). The **⚡ Installation automatique (root)** button stays for
|
|
||||||
re-runs. The "already onboarded" flag is persisted per host (SharedPreferences)
|
|
||||||
so reopening the app doesn't redo it. Steps:
|
|
||||||
|
|
||||||
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
|
|
||||||
`subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of
|
|
||||||
the trust store over `/system/etc/security/cacerts` (+ the conscrypt
|
|
||||||
APEX path on Android 14), restoring the SELinux context
|
|
||||||
(`u:object_r:system_security_cacerts_file:s0`). **Every** app trusts the
|
|
||||||
cabine CA — not just user-CA opt-in apps. Reversible via `umount`.
|
|
||||||
2. **Native WireGuard** — if the kernel has the WireGuard module + `wg`/`ip`,
|
|
||||||
brings the tunnel up natively (`ip link add … type wireguard` + `wg set`),
|
|
||||||
no WireGuard app required.
|
|
||||||
3. **Auto R3 verify** — polls `/wg/r3-check`.
|
|
||||||
|
|
||||||
**Fallback** — if the kernel lacks WireGuard, the root path installs the
|
|
||||||
system CA then hands off to the manual WireGuard-app flow (steps 3–5 above).
|
|
||||||
|
|
||||||
All root actions are **gated behind the explicit tap** — nothing runs as
|
|
||||||
root without the operator choosing root mode on their own device.
|
|
||||||
See `RootShell.kt` (su wrapper) and `RootOnboard.kt` (silent sequence).
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
|
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
|
||||||
- **GitHub Actions** `build-android-apk.yml` → debug APK artifact.
|
- **GitHub Actions** `build-android-apk.yml` → debug APK artifact.
|
||||||
|
|
@ -48,14 +22,12 @@ gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Constraints (MVP)
|
## Constraints (MVP)
|
||||||
- Android 11+ restricts **user CA trust** ; the *manual* path launches the
|
- Android 11+ restricts **user CA trust** ; the app launches the install
|
||||||
install intent + guides the confirm step. Browsers on the device need the
|
intent + guides the manual confirm step. Browsers on the device need
|
||||||
CA trusted for the mitm R3 break — this is the known Android limitation on
|
the CA trusted for the mitm R3 break — this is the known Android
|
||||||
non-rooted devices. **Rooted devices bypass it entirely** via the system
|
limitation (documented, not yet automated).
|
||||||
CA install (see Root path above).
|
- WireGuard profile import uses the **official WireGuard app** (no embedded
|
||||||
- The *manual* path imports the WireGuard profile via the **official
|
tunnel in the MVP) — most reliable, no extra native deps.
|
||||||
WireGuard app** (no embedded tunnel) — most reliable, no extra native
|
|
||||||
deps. The *root* path brings the tunnel up natively with the kernel module.
|
|
||||||
- Debug APK is self-signed (sideload). Release signing (published
|
- Debug APK is self-signed (sideload). Release signing (published
|
||||||
fingerprint, served from the toolbox) is a follow-up needing a keystore
|
fingerprint, served from the toolbox) is a follow-up needing a keystore
|
||||||
secret in CI.
|
secret in CI.
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ android {
|
||||||
applicationId = "in.secubox.toolbox"
|
applicationId = "in.secubox.toolbox"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 2
|
versionCode = 1
|
||||||
versionName = "0.2.0"
|
versionName = "0.1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import android.provider.Settings
|
||||||
import android.security.KeyChain
|
import android.security.KeyChain
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
@ -41,7 +40,7 @@ private val Matrix = Color(0xFF00FF41)
|
||||||
private val Cinnabar = Color(0xFFE63946)
|
private val Cinnabar = Color(0xFFE63946)
|
||||||
private val TextPrimary = Color(0xFFE8E6D9)
|
private val TextPrimary = Color(0xFFE8E6D9)
|
||||||
|
|
||||||
enum class Step { Discover, RootAuto, InstallCa, ImportProfile, Verify, Done }
|
enum class Step { Discover, InstallCa, ImportProfile, Verify, Done }
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
@ -62,52 +61,6 @@ fun OnboardApp() {
|
||||||
var onTunnel by remember { mutableStateOf(false) }
|
var onTunnel by remember { mutableStateOf(false) }
|
||||||
var peerIp by remember { mutableStateOf<String?>(null) }
|
var peerIp by remember { mutableStateOf<String?>(null) }
|
||||||
val api = remember(host) { ToolboxApi(host) }
|
val api = remember(host) { ToolboxApi(host) }
|
||||||
var rootAvail by remember { mutableStateOf(false) }
|
|
||||||
val rootLog = remember { mutableStateListOf<String>() }
|
|
||||||
val prefs = remember {
|
|
||||||
ctx.getSharedPreferences("secubox-toolbox", android.content.Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
var autoTried by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// The whole root-mode silent run, reused by the ⚡ button AND the
|
|
||||||
// zero-tap auto-launch (#551). Persists an onboarded flag per host on
|
|
||||||
// success so reopening the app doesn't redo it.
|
|
||||||
val runRootAuto: () -> Unit = {
|
|
||||||
busy = true; status = ""; rootLog.clear()
|
|
||||||
scope.launch {
|
|
||||||
val ok = withContext(Dispatchers.IO) { api.reachable() }
|
|
||||||
if (!ok) {
|
|
||||||
busy = false; status = "Borne injoignable — vérifie le réseau."
|
|
||||||
} else {
|
|
||||||
step = Step.RootAuto
|
|
||||||
val onb = RootOnboard(api, ctx.cacheDir)
|
|
||||||
val out = withContext(Dispatchers.IO) {
|
|
||||||
onb.runSilent { line -> scope.launch(Dispatchers.Main) { rootLog.add(line) } }
|
|
||||||
}
|
|
||||||
busy = false
|
|
||||||
onTunnel = out.verified
|
|
||||||
if (out.verified) prefs.edit().putBoolean("onboarded:$host", true).apply()
|
|
||||||
when {
|
|
||||||
out.verified -> step = Step.Done
|
|
||||||
out.wgViaApp -> { step = Step.ImportProfile
|
|
||||||
status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." }
|
|
||||||
else -> { step = Step.Verify
|
|
||||||
status = "Active le tunnel puis vérifie." }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect root once, off the main thread.
|
|
||||||
LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } }
|
|
||||||
// Zero-tap (#551): on a rooted device, auto-run the silent onboarding
|
|
||||||
// once on launch — unless this host was already onboarded.
|
|
||||||
LaunchedEffect(rootAvail) {
|
|
||||||
if (rootAvail && !autoTried && step == Step.Discover) {
|
|
||||||
autoTried = true
|
|
||||||
if (!prefs.getBoolean("onboarded:$host", false)) runRootAuto()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme(
|
MaterialTheme(colorScheme = darkColorScheme(
|
||||||
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
|
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
|
||||||
|
|
@ -145,36 +98,6 @@ fun OnboardApp() {
|
||||||
else status = "Borne injoignable — vérifie l'adresse / le réseau."
|
else status = "Borne injoignable — vérifie l'adresse / le réseau."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rootAvail) {
|
|
||||||
Spacer(Modifier.height(10.dp))
|
|
||||||
Text("🔓 Root détecté — l'installation se lance automatiquement. " +
|
|
||||||
"Tu peux aussi la relancer ici.",
|
|
||||||
color = Matrix, fontSize = 12.sp)
|
|
||||||
Spacer(Modifier.height(6.dp))
|
|
||||||
OutlinedButton(onClick = runRootAuto, modifier = Modifier.fillMaxWidth(),
|
|
||||||
border = BorderStroke(1.dp, Matrix),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = Matrix)) {
|
|
||||||
Text("⚡ Installation automatique (root)", fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Step.RootAuto -> {
|
|
||||||
StepBody("Installation automatique (root)",
|
|
||||||
"CA système + tunnel WireGuard, sans intervention.")
|
|
||||||
Surface(color = Color(0xFF0E0E15), shape = MaterialTheme.shapes.small,
|
|
||||||
modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Column(Modifier.padding(12.dp)) {
|
|
||||||
rootLog.forEach { line ->
|
|
||||||
Text(line, color = if (line.startsWith("✗")) Cinnabar
|
|
||||||
else if (line.startsWith("✓")) Matrix else TextPrimary,
|
|
||||||
fontSize = 12.sp, fontFamily = FontFamily.Monospace)
|
|
||||||
}
|
|
||||||
if (busy) {
|
|
||||||
Spacer(Modifier.height(6.dp))
|
|
||||||
CircularProgressIndicator(Modifier.size(18.dp), color = Gold, strokeWidth = 2.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Step.InstallCa -> {
|
Step.InstallCa -> {
|
||||||
StepBody("1 · Installer le certificat (CA R3)",
|
StepBody("1 · Installer le certificat (CA R3)",
|
||||||
|
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
//
|
|
||||||
// Root-mode fully-automated silent R3 onboarding (#538).
|
|
||||||
// All actions are gated behind an explicit "root auto" tap in the UI.
|
|
||||||
package `in`.secubox.toolbox
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
|
|
||||||
class RootOnboard(private val api: ToolboxApi, private val cacheDir: File) {
|
|
||||||
|
|
||||||
/** A line appended to the on-screen log during the silent run. */
|
|
||||||
fun interface Logger { fun log(line: String) }
|
|
||||||
|
|
||||||
data class Outcome(val caInstalled: Boolean, val wgUp: Boolean, val verified: Boolean,
|
|
||||||
val wgViaApp: Boolean)
|
|
||||||
|
|
||||||
// ── system CA install (silent, root) ──
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenSSL `subject_hash_old` (pre-1.0 hash) computed WITHOUT openssl :
|
|
||||||
* MD5 of the DER-encoded subject name, first 4 bytes as a uint32
|
|
||||||
* little-endian, formatted "%08x". This is the filename the Android
|
|
||||||
* system cacerts store uses (<hash>.0).
|
|
||||||
*/
|
|
||||||
fun subjectHashOld(pem: ByteArray): String {
|
|
||||||
val cf = CertificateFactory.getInstance("X.509")
|
|
||||||
val cert = cf.generateCertificate(pem.inputStream()) as java.security.cert.X509Certificate
|
|
||||||
val subjectDer = cert.subjectX500Principal.encoded
|
|
||||||
val md5 = MessageDigest.getInstance("MD5").digest(subjectDer)
|
|
||||||
val h = (md5[0].toLong() and 0xff) or
|
|
||||||
((md5[1].toLong() and 0xff) shl 8) or
|
|
||||||
((md5[2].toLong() and 0xff) shl 16) or
|
|
||||||
((md5[3].toLong() and 0xff) shl 24)
|
|
||||||
return String.format("%08x", h)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install the CA into the SYSTEM trust store so EVERY app (not just
|
|
||||||
* those opting into user CAs) trusts it. Uses the bind-mount-over-
|
|
||||||
* cacerts technique that works on Android 10–14 (incl. the conscrypt
|
|
||||||
* APEX). Non-persistent across reboot — fine for a temporary cabine
|
|
||||||
* diagnostic; the app can also unmount to revert.
|
|
||||||
*/
|
|
||||||
fun installCaSystem(log: Logger): Boolean {
|
|
||||||
log.log("• Téléchargement du CA…")
|
|
||||||
val pem = api.download("/wg/ca.pem", "village3b-ca.pem", cacheDir).readBytes()
|
|
||||||
val hash = subjectHashOld(pem)
|
|
||||||
log.log("• CA hash : $hash.0")
|
|
||||||
val local = File(cacheDir, "$hash.0").apply { writeBytes(pem) }
|
|
||||||
|
|
||||||
// Push the cert to a root-readable scratch path, then bind-mount a
|
|
||||||
// populated copy of the system store over the live cacerts dir.
|
|
||||||
val pushed = "/data/local/tmp/sbx-$hash.0"
|
|
||||||
val push = RootShell.install(local, pushed, "644")
|
|
||||||
if (!push.ok) { log.log("✗ push échoué : ${push.err.trim()}"); return false }
|
|
||||||
|
|
||||||
val r = RootShell.runScript(
|
|
||||||
"set -e",
|
|
||||||
"CERT_DIR=/system/etc/security/cacerts",
|
|
||||||
"TMP=/data/local/tmp/sbx-cacerts",
|
|
||||||
"rm -rf \$TMP; mkdir -p \$TMP",
|
|
||||||
// seed with the existing system + APEX certs so nothing is lost
|
|
||||||
"cp -f \$CERT_DIR/* \$TMP/ 2>/dev/null || true",
|
|
||||||
"cp -f /apex/com.android.conscrypt/cacerts/* \$TMP/ 2>/dev/null || true",
|
|
||||||
"cp -f $pushed \$TMP/$hash.0",
|
|
||||||
"chmod 644 \$TMP/* 2>/dev/null || true",
|
|
||||||
"chown 0:0 \$TMP/* 2>/dev/null || true",
|
|
||||||
"chcon u:object_r:system_security_cacerts_file:s0 \$TMP/* 2>/dev/null || true",
|
|
||||||
// bind-mount over the live store (and the APEX path on 14)
|
|
||||||
"mount -o bind \$TMP \$CERT_DIR",
|
|
||||||
"[ -d /apex/com.android.conscrypt/cacerts ] && mount -o bind \$TMP /apex/com.android.conscrypt/cacerts 2>/dev/null || true",
|
|
||||||
"echo OK",
|
|
||||||
)
|
|
||||||
if (r.ok && r.out.contains("OK")) {
|
|
||||||
log.log("✓ CA installé dans le magasin système (toutes les apps le font confiance)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
log.log("✗ install CA système : ${r.err.trim().ifBlank { r.out.trim() }}")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeCaSystem(log: Logger): Boolean {
|
|
||||||
val r = RootShell.runScript(
|
|
||||||
"umount /system/etc/security/cacerts 2>/dev/null || true",
|
|
||||||
"umount /apex/com.android.conscrypt/cacerts 2>/dev/null || true",
|
|
||||||
"echo OK",
|
|
||||||
)
|
|
||||||
log.log(if (r.ok) "✓ CA système retiré (démonté)" else "✗ démontage : ${r.err.trim()}")
|
|
||||||
return r.ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── WireGuard bring-up ──
|
|
||||||
|
|
||||||
/** Parse the wg-quick .conf into the fields we need. */
|
|
||||||
private data class WgConf(val privKey: String, val address: String,
|
|
||||||
val pubKey: String, val endpoint: String, val allowed: String)
|
|
||||||
|
|
||||||
private fun parse(conf: String): WgConf? {
|
|
||||||
var pk = ""; var addr = ""; var pub = ""; var ep = ""; var aip = ""
|
|
||||||
conf.lineSequence().forEach { raw ->
|
|
||||||
val l = raw.trim()
|
|
||||||
when {
|
|
||||||
l.startsWith("PrivateKey", true) -> pk = l.substringAfter("=").trim()
|
|
||||||
l.startsWith("Address", true) -> addr = l.substringAfter("=").trim()
|
|
||||||
l.startsWith("PublicKey", true) -> pub = l.substringAfter("=").trim()
|
|
||||||
l.startsWith("Endpoint", true) -> ep = l.substringAfter("=").trim()
|
|
||||||
l.startsWith("AllowedIPs", true) -> aip = l.substringAfter("=").trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (pk.isNotBlank() && pub.isNotBlank() && ep.isNotBlank()) WgConf(pk, addr, pub, ep, aip) else null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bring the tunnel up natively with root IF the kernel has WireGuard
|
|
||||||
* + `wg`/`ip`. Returns true on success ; false means the caller
|
|
||||||
* should fall back to the WireGuard-app handoff.
|
|
||||||
*/
|
|
||||||
fun setupWireguardRoot(log: Logger): Boolean {
|
|
||||||
if (!RootShell.hasKernelWireguard()) {
|
|
||||||
log.log("• Noyau sans module WireGuard — bascule sur l'app WireGuard")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
log.log("• Génération du profil WireGuard…")
|
|
||||||
val conf = api.downloadProfile(cacheDir).readText()
|
|
||||||
val wg = parse(conf) ?: run { log.log("✗ profil illisible"); return false }
|
|
||||||
val iface = "wg-village3b"
|
|
||||||
val r = RootShell.runScript(
|
|
||||||
"set -e",
|
|
||||||
"ip link del $iface 2>/dev/null || true",
|
|
||||||
"ip link add $iface type wireguard",
|
|
||||||
"echo '${wg.privKey}' > /data/local/tmp/sbx-wg.key && chmod 600 /data/local/tmp/sbx-wg.key",
|
|
||||||
"wg set $iface private-key /data/local/tmp/sbx-wg.key peer ${wg.pubKey} endpoint ${wg.endpoint} allowed-ips ${wg.allowed.ifBlank { "0.0.0.0/0" }} persistent-keepalive 25",
|
|
||||||
"rm -f /data/local/tmp/sbx-wg.key",
|
|
||||||
if (wg.address.isNotBlank()) "ip addr add ${wg.address} dev $iface 2>/dev/null || true" else ":",
|
|
||||||
"ip link set $iface up",
|
|
||||||
"for n in ${wg.allowed.replace(",", " ")}; do ip route replace \$n dev $iface 2>/dev/null || true; done",
|
|
||||||
"echo OK",
|
|
||||||
)
|
|
||||||
if (r.ok && r.out.contains("OK")) { log.log("✓ Tunnel $iface actif (root, natif)"); return true }
|
|
||||||
log.log("✗ WG natif : ${r.err.trim().ifBlank { r.out.trim() }} — bascule sur l'app")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Run the whole silent sequence. Blocking — call off-main. */
|
|
||||||
fun runSilent(log: Logger): Outcome {
|
|
||||||
val ca = installCaSystem(log)
|
|
||||||
val wgRoot = setupWireguardRoot(log)
|
|
||||||
var verified = false
|
|
||||||
if (wgRoot) {
|
|
||||||
log.log("• Vérification R3…")
|
|
||||||
Thread.sleep(1500)
|
|
||||||
val (t, ip) = api.r3Check()
|
|
||||||
verified = t
|
|
||||||
log.log(if (t) "✓ Tunnel R3 confirmé (${ip ?: "?"})" else "• Pas encore confirmé — réessaie la vérification")
|
|
||||||
}
|
|
||||||
return Outcome(caInstalled = ca, wgUp = wgRoot, verified = verified, wgViaApp = !wgRoot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
package `in`.secubox.toolbox
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thin wrapper around `su` for the root-mode silent onboarding (#538).
|
|
||||||
* Every action is gated behind an explicit user tap in the UI — nothing
|
|
||||||
* runs as root without the operator choosing root mode on their own
|
|
||||||
* device.
|
|
||||||
*/
|
|
||||||
object RootShell {
|
|
||||||
|
|
||||||
data class Result(val code: Int, val out: String, val err: String) {
|
|
||||||
val ok get() = code == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/** True if a `su` binary is on PATH and grants a root shell. */
|
|
||||||
fun available(): Boolean = try {
|
|
||||||
run("id -u").out.trim() == "0"
|
|
||||||
} catch (_: Exception) { false }
|
|
||||||
|
|
||||||
/** Run a single command in a root shell. Blocking — call off-main. */
|
|
||||||
fun run(cmd: String): Result {
|
|
||||||
val p = ProcessBuilder("su", "-c", cmd)
|
|
||||||
.redirectErrorStream(false)
|
|
||||||
.start()
|
|
||||||
val out = p.inputStream.bufferedReader().readText()
|
|
||||||
val err = p.errorStream.bufferedReader().readText()
|
|
||||||
val code = p.waitFor()
|
|
||||||
return Result(code, out, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Run several commands in ONE root shell (atomic-ish, keeps remount). */
|
|
||||||
fun runScript(vararg lines: String): Result {
|
|
||||||
val p = ProcessBuilder("su").redirectErrorStream(false).start()
|
|
||||||
p.outputStream.bufferedWriter().use { w ->
|
|
||||||
lines.forEach { w.write(it); w.write("\n") }
|
|
||||||
w.write("exit $?\n")
|
|
||||||
}
|
|
||||||
val out = p.inputStream.bufferedReader().readText()
|
|
||||||
val err = p.errorStream.bufferedReader().readText()
|
|
||||||
val code = p.waitFor()
|
|
||||||
return Result(code, out, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Push a local file to a root-owned path via cat (avoids cp quirks). */
|
|
||||||
fun install(src: File, destPath: String, mode: String = "644"): Result {
|
|
||||||
val b64 = android.util.Base64.encodeToString(src.readBytes(), android.util.Base64.NO_WRAP)
|
|
||||||
return runScript(
|
|
||||||
"echo '$b64' | base64 -d > '$destPath'",
|
|
||||||
"chmod $mode '$destPath'",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Kernel has WireGuard + the `wg` tool available to root? */
|
|
||||||
fun hasKernelWireguard(): Boolean = try {
|
|
||||||
val w = run("command -v wg || ls /system/*bin/wg 2>/dev/null")
|
|
||||||
val ip = run("command -v ip")
|
|
||||||
w.out.isNotBlank() && ip.out.isNotBlank()
|
|
||||||
} catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
5
clients/webext-toolbox/.gitignore
vendored
5
clients/webext-toolbox/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
# build artefacts
|
|
||||||
*.xpi
|
|
||||||
*.zip
|
|
||||||
*.crx
|
|
||||||
web-ext-artifacts/
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
|
||||||
# SecuBox ToolBoX — browser extension (Cartographie sociale, #532)
|
|
||||||
|
|
||||||
A WebExtension (Firefox `.xpi` + Chromium MV3) that **emancipates** the R3
|
|
||||||
toolbox live tracker analysis into the browser: instead of only seeing the
|
|
||||||
*cartographie sociale* on `kbin/social/me`, a toolbar badge ticks up as
|
|
||||||
trackers fire, and a popup shows who is watching you — live.
|
|
||||||
|
|
||||||
Sibling of [`clients/android-toolbox/`](../android-toolbox/). Talks **only**
|
|
||||||
to your cabine over the R3 tunnel — no third-party calls.
|
|
||||||
|
|
||||||
## What it does
|
|
||||||
|
|
||||||
- **Pairing** — calls `/social/me` over the tunnel, which 303-redirects to
|
|
||||||
`/social/{token}`; the extension reads the minted HMAC token from the
|
|
||||||
final URL. Anonymous (rotating `mac_hash`), no account. Manual token entry
|
|
||||||
available in the options page.
|
|
||||||
- **Live badge** — the toolbar icon shows the live tracker count for the
|
|
||||||
session (polled once a minute). Colour escalates: gold → 🟥 anti-bot
|
|
||||||
present → 🟪 operator-grade present.
|
|
||||||
- **Popup** — four stat tiles (trackers / sites / anti-bot / operator-grade),
|
|
||||||
a dependency-free **mini Round-Eye graph** (device centre, trackers on the
|
|
||||||
ring, radius by hits, colour by tier), and a top-tracker list with CDN
|
|
||||||
(12.A) / anti-bot (12.B) / operator-grade (12.C) tags.
|
|
||||||
- **Actions** — *Cartographie complète* (opens the full d3 view at
|
|
||||||
`/social/{token}`), *Rapport PDF* (`/social/report/{token}.pdf`), and
|
|
||||||
*Effacer mes données* (RGPD art. 17 wipe → `POST /social/wipe/{token}`).
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Published release `.xpi` (downloadable directly):
|
|
||||||
|
|
||||||
```
|
|
||||||
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.1/secubox-toolbox-webext.xpi
|
|
||||||
```
|
|
||||||
|
|
||||||
The toolbox also serves it from the cabine:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://kbin.<board>.secubox.in/wg/toolbox.xpi
|
|
||||||
```
|
|
||||||
|
|
||||||
The kbin onboard panel exposes a **🧩 Extension navigateur (cartographie)**
|
|
||||||
button. When a local build is present the cabine serves it; otherwise it
|
|
||||||
302-redirects to the **tag-pinned** release asset above. The webext release
|
|
||||||
is published `make_latest:false` so it does not steal the repo "Latest"
|
|
||||||
pointer from the Android APK release (whose endpoint resolves via
|
|
||||||
`/releases/latest/download/…`) — bump the tag in the `/wg/toolbox.xpi`
|
|
||||||
endpoint constant + `secubox-toolbox-fetch-xpi` when a new `webext-v*`
|
|
||||||
release is cut.
|
|
||||||
|
|
||||||
- **Firefox** — open the `.xpi`. A permanent install needs an AMO-signed
|
|
||||||
build (release CI step / `web-ext sign`); for development use
|
|
||||||
*about:debugging → Load Temporary Add-on*, or an ESR/Dev build with
|
|
||||||
`xpinstall.signatures.required=false`.
|
|
||||||
- **Linux Firefox (fast)** — one call grabs the `.xpi` and launches Firefox
|
|
||||||
with it loaded (via `web-ext run`, no signing needed):
|
|
||||||
```bash
|
|
||||||
clients/webext-toolbox/install-firefox-linux.sh # from kbin.gk2.secubox.in
|
|
||||||
clients/webext-toolbox/install-firefox-linux.sh --release # from the GitHub release
|
|
||||||
clients/webext-toolbox/install-firefox-linux.sh --local # from this checkout
|
|
||||||
```
|
|
||||||
- **Chromium** — load unpacked (`chrome://extensions` → Developer mode).
|
|
||||||
Ships rasterised PNG icons (`icons/icon-48/128.png`), so it loads as-is.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
No bundler — the extension is plain JS/HTML/CSS. CI zips it:
|
|
||||||
|
|
||||||
- GitHub Actions `build-webext.yml` → `.xpi` artifact on push to `master` /
|
|
||||||
PRs touching `clients/webext-toolbox/**`; tagging `webext-v*` publishes the
|
|
||||||
`.xpi` as a release asset.
|
|
||||||
|
|
||||||
Locally:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd clients/webext-toolbox
|
|
||||||
./build.sh # → secubox-toolbox-webext-<version>.xpi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
| File | Role |
|
|
||||||
|------|------|
|
|
||||||
| `manifest.json` | MV3, cross-browser background (`service_worker` + `scripts`) |
|
|
||||||
| `api.js` | shared client over `/wg/r3-check`, `/social/*` |
|
|
||||||
| `background.js` | badge sync + silent re-pair (SW or event page) |
|
|
||||||
| `popup/` | live view, mini graph (`graph.js`), actions |
|
|
||||||
| `options/` | host / window / manual token |
|
|
||||||
|
|
||||||
## Cabine endpoints consumed
|
|
||||||
|
|
||||||
| Endpoint | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `/wg/r3-check` | tunnel presence indicator |
|
|
||||||
| `/social/me` | pair → mint token (303 → `/social/{token}`) |
|
|
||||||
| `/social/graph/{token}?since=` | per-session tracker graph JSON |
|
|
||||||
| `/social/wipe/{token}` | RGPD art. 17 erasure |
|
|
||||||
| `/social/{token}` | full d3 cartographie page |
|
|
||||||
| `/social/report/{token}.pdf` | bilingual PDF report |
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- No server-side CORS needed: an MV3 extension with `host_permissions` for
|
|
||||||
`*.secubox.in` fetches cross-origin from its background without CORS.
|
|
||||||
- MVP polls `/social/graph` and computes the delta client-side; a future
|
|
||||||
`GET /social/live/{token}` (SSE) can replace the poll. The deception-plane
|
|
||||||
*Poke/Emancipate* per-site control lands once #525 ships.
|
|
||||||
|
|
||||||
License `LicenseRef-CMSD-1.0`.
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
//
|
|
||||||
// SecuBox-Deb :: webext-toolbox :: api
|
|
||||||
// Thin client over the R3 toolbox social endpoints. Shared by the
|
|
||||||
// background service worker and the popup. Cross-origin fetches are
|
|
||||||
// allowed because the extension holds host_permissions for the cabine
|
|
||||||
// vhosts — no server-side CORS needed.
|
|
||||||
|
|
||||||
// browser (Firefox promise API) || chrome (Chromium / FF MV3 SW)
|
|
||||||
const ext = globalThis.browser || globalThis.chrome;
|
|
||||||
|
|
||||||
const DEFAULTS = {
|
|
||||||
host: "kbin.gk2.secubox.in",
|
|
||||||
token: "",
|
|
||||||
since: 86400,
|
|
||||||
};
|
|
||||||
|
|
||||||
// base URL from a stored host (accept bare host or full origin)
|
|
||||||
function baseUrl(host) {
|
|
||||||
const h = (host || DEFAULTS.host).trim().replace(/\/+$/, "");
|
|
||||||
if (/^https?:\/\//i.test(h)) return h;
|
|
||||||
return `https://${h}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getConfig() {
|
|
||||||
const stored = await ext.storage.local.get(["host", "token", "since"]);
|
|
||||||
return { ...DEFAULTS, ...stored };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setConfig(patch) {
|
|
||||||
await ext.storage.local.set(patch);
|
|
||||||
return getConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the HMAC token from a /social/{token} URL path.
|
|
||||||
function tokenFromUrl(url) {
|
|
||||||
try {
|
|
||||||
const u = new URL(url);
|
|
||||||
const m = u.pathname.match(/\/social\/([^/?#]+)/);
|
|
||||||
if (m && m[1] !== "me" && m[1].split(".").length === 4) return m[1];
|
|
||||||
} catch (_) {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pair: hit /social/me over the tunnel; it 303-redirects to
|
|
||||||
// /social/{token}. fetch follows the redirect, so response.url carries
|
|
||||||
// the minted token. Returns the token or throws.
|
|
||||||
async function pair(host) {
|
|
||||||
const url = `${baseUrl(host)}/social/me`;
|
|
||||||
const resp = await fetch(url, { redirect: "follow", credentials: "omit" });
|
|
||||||
const tok = tokenFromUrl(resp.url);
|
|
||||||
if (!tok) throw new Error("pairing failed — not on the R3 tunnel?");
|
|
||||||
return tok;
|
|
||||||
}
|
|
||||||
|
|
||||||
// r3-check: is this client on the R3 tunnel right now?
|
|
||||||
async function r3Check(host) {
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`${baseUrl(host)}/wg/r3-check`, { credentials: "omit" });
|
|
||||||
if (!resp.ok) return { tunnel: false, peer_ip: null };
|
|
||||||
return await resp.json();
|
|
||||||
} catch (_) {
|
|
||||||
return { tunnel: false, peer_ip: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// graph: the per-session cartographie JSON. Throws on HTTP error so the
|
|
||||||
// caller can show "token expired — re-pair".
|
|
||||||
async function graph(host, token, since) {
|
|
||||||
const qs = new URLSearchParams({ since: String(since || DEFAULTS.since) });
|
|
||||||
const resp = await fetch(`${baseUrl(host)}/social/graph/${token}?${qs}`, {
|
|
||||||
credentials: "omit",
|
|
||||||
});
|
|
||||||
if (resp.status === 403 || resp.status === 404) {
|
|
||||||
throw new Error("token-expired");
|
|
||||||
}
|
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
||||||
return await resp.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
// RGPD art.17 wipe.
|
|
||||||
async function wipe(host, token) {
|
|
||||||
const resp = await fetch(`${baseUrl(host)}/social/wipe/${token}`, {
|
|
||||||
method: "POST",
|
|
||||||
credentials: "omit",
|
|
||||||
});
|
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
||||||
return await resp.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
function socialUrl(host, token) {
|
|
||||||
return `${baseUrl(host)}/social/${token}`;
|
|
||||||
}
|
|
||||||
function reportUrl(host, token) {
|
|
||||||
return `${baseUrl(host)}/social/report/${token}.pdf`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SbxApi = {
|
|
||||||
DEFAULTS,
|
|
||||||
ext,
|
|
||||||
baseUrl,
|
|
||||||
getConfig,
|
|
||||||
setConfig,
|
|
||||||
pair,
|
|
||||||
r3Check,
|
|
||||||
graph,
|
|
||||||
wipe,
|
|
||||||
socialUrl,
|
|
||||||
reportUrl,
|
|
||||||
tokenFromUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Usable both as a classic background script (globalThis) and an ES-less
|
|
||||||
// service worker. No module syntax to stay loadable as plain script.
|
|
||||||
globalThis.SbxApi = SbxApi;
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
//
|
|
||||||
// SecuBox-Deb :: webext-toolbox :: background
|
|
||||||
// Keeps the toolbar badge in sync with the live tracker count and
|
|
||||||
// re-pairs over the R3 tunnel when the token expires. Works as a
|
|
||||||
// Chromium MV3 service worker (importScripts) AND a Firefox event page
|
|
||||||
// (api.js preloaded via background.scripts).
|
|
||||||
|
|
||||||
if (typeof importScripts === "function") {
|
|
||||||
// Chromium service worker: api.js isn't auto-loaded, pull it in.
|
|
||||||
try { importScripts("api.js"); } catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NB: do NOT declare `const ext` here — api.js already declares it in the
|
|
||||||
// shared script scope (event page) / worker global (importScripts), and a
|
|
||||||
// second `const ext` is a "redeclaration of const ext" SyntaxError that
|
|
||||||
// kills the whole background script. Use api.ext instead.
|
|
||||||
const api = globalThis.SbxApi;
|
|
||||||
|
|
||||||
const ALARM = "sbx-refresh";
|
|
||||||
const PERIOD_MIN = 1; // poll the cabine once a minute
|
|
||||||
|
|
||||||
function setBadge(text, color) {
|
|
||||||
try {
|
|
||||||
api.ext.action.setBadgeText({ text: text || "" });
|
|
||||||
if (color) ext.action.setBadgeBackgroundColor({ color });
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pull the graph, update the badge with the live tracker count. Auto
|
|
||||||
// re-pairs once if the stored token has expired.
|
|
||||||
async function refresh() {
|
|
||||||
const cfg = await api.getConfig();
|
|
||||||
if (!cfg.host) { setBadge("", "#6b6b7a"); return; }
|
|
||||||
|
|
||||||
let token = cfg.token;
|
|
||||||
const run = async (tok) => api.graph(cfg.host, tok, cfg.since);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!token) token = await api.pair(cfg.host);
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = await run(token);
|
|
||||||
} catch (e) {
|
|
||||||
if (String(e.message) === "token-expired") {
|
|
||||||
token = await api.pair(cfg.host); // one silent re-pair
|
|
||||||
data = await run(token);
|
|
||||||
} else throw e;
|
|
||||||
}
|
|
||||||
await api.setConfig({ token });
|
|
||||||
const n = (data.stats && data.stats.total_trackers) || 0;
|
|
||||||
// colour escalates with operator-grade / anti-bot presence
|
|
||||||
const opg = (data.stats && data.stats.opgrade_sites) || 0;
|
|
||||||
const ab = (data.stats && data.stats.antibot_sites) || 0;
|
|
||||||
const color = opg > 0 ? "#6e40c9" : ab > 0 ? "#e63946" : "#c9a84c";
|
|
||||||
setBadge(n > 999 ? "999+" : String(n), color);
|
|
||||||
await ext.storage.local.set({ lastStats: data.stats || {}, lastError: "" });
|
|
||||||
} catch (e) {
|
|
||||||
setBadge("!", "#6b6b7a");
|
|
||||||
await ext.storage.local.set({ lastError: String(e.message || e) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.ext.runtime.onInstalled.addListener(() => {
|
|
||||||
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
|
||||||
refresh();
|
|
||||||
});
|
|
||||||
api.ext.runtime.onStartup && api.ext.runtime.onStartup.addListener(() => {
|
|
||||||
api.ext.alarms.create(ALARM, { periodInMinutes: PERIOD_MIN });
|
|
||||||
refresh();
|
|
||||||
});
|
|
||||||
api.ext.alarms.onAlarm.addListener((a) => { if (a.name === ALARM) refresh(); });
|
|
||||||
|
|
||||||
// popup asks for an immediate refresh after pairing / config change
|
|
||||||
api.ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
||||||
if (msg && msg.type === "refresh") {
|
|
||||||
refresh().then(() => sendResponse({ ok: true }));
|
|
||||||
return true; // async response
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
#
|
|
||||||
# SecuBox ToolBoX Cartographie — build the unsigned .xpi (a zip of the
|
|
||||||
# extension dir). Firefox loads it as-is (temporary add-on / ESR with
|
|
||||||
# signatures off) ; a release build signs it via web-ext / AMO.
|
|
||||||
# Usage: ./build.sh → produces ./secubox-toolbox-webext-<version>.xpi
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
VER=$(grep -oE '"version"[^,]*' manifest.json | grep -oE '[0-9.]+' | head -1)
|
|
||||||
OUT="secubox-toolbox-webext-${VER}.xpi"
|
|
||||||
rm -f "$OUT"
|
|
||||||
|
|
||||||
# -FS = sync (drop stale entries) ; exclude VCS, dotfiles, build script,
|
|
||||||
# any previously built artefact, docs, and the SVG icon source (only the
|
|
||||||
# rasterised PNGs are referenced by the manifest — keep SVG out of the
|
|
||||||
# package so Firefox never renders it in chrome UI).
|
|
||||||
zip -r -FS "$OUT" . \
|
|
||||||
-x '*.git*' '*/.*' 'build.sh' '*.xpi' 'README.md' 'icons/icon.svg' >/dev/null
|
|
||||||
|
|
||||||
echo "built $OUT ($(stat -c%s "$OUT" 2>/dev/null || stat -f%z "$OUT") bytes)"
|
|
||||||
echo "Firefox: about:debugging → This Firefox → Load Temporary Add-on → pick the .xpi (or manifest.json)."
|
|
||||||
echo "Permanent install needs signing (web-ext sign / AMO) or Dev/ESR with xpinstall.signatures.required=false."
|
|
||||||
echo "Chromium: action icons must be raster — rasterise icons/icon.svg to PNG before a Chromium store build."
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 618 B |
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
|
||||||
<rect width="128" height="128" rx="24" fill="#0a0a0f"/>
|
|
||||||
<!-- outer eye almond -->
|
|
||||||
<path d="M64 36 C92 36 114 64 114 64 C114 64 92 92 64 92 C36 92 14 64 14 64 C14 64 36 36 64 36 Z"
|
|
||||||
fill="none" stroke="#c9a84c" stroke-width="5"/>
|
|
||||||
<!-- iris -->
|
|
||||||
<circle cx="64" cy="64" r="20" fill="#0c0c12" stroke="#00ff41" stroke-width="4"/>
|
|
||||||
<circle cx="64" cy="64" r="7" fill="#00ff41"/>
|
|
||||||
<!-- tracker spokes -->
|
|
||||||
<g stroke="#6e40c9" stroke-width="3" opacity="0.8">
|
|
||||||
<line x1="64" y1="64" x2="104" y2="40"/>
|
|
||||||
<line x1="64" y1="64" x2="24" y2="40"/>
|
|
||||||
<line x1="64" y1="64" x2="100" y2="92"/>
|
|
||||||
</g>
|
|
||||||
<g fill="#00d4ff">
|
|
||||||
<circle cx="104" cy="40" r="5"/>
|
|
||||||
<circle cx="24" cy="40" r="5"/>
|
|
||||||
</g>
|
|
||||||
<circle cx="100" cy="92" r="5" fill="#e63946"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 951 B |
|
|
@ -1,87 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
#
|
|
||||||
# SecuBox ToolBoX — Linux Firefox installer (#547)
|
|
||||||
# One call: grab the ToolBoX cartographie extension and launch Firefox with
|
|
||||||
# it loaded. Prefers `web-ext run` (temporary load, works unsigned — fastest)
|
|
||||||
# and falls back to opening the .xpi for the install prompt.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./install-firefox-linux.sh # from kbin.gk2.secubox.in
|
|
||||||
# ./install-firefox-linux.sh kbin.my.box # from another cabine host
|
|
||||||
# ./install-firefox-linux.sh --release # from the latest GitHub release
|
|
||||||
# ./install-firefox-linux.sh --local # build from this checkout (web-ext)
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DEFAULT_HOST="kbin.gk2.secubox.in"
|
|
||||||
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.1/secubox-toolbox-webext.xpi"
|
|
||||||
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
|
|
||||||
say(){ printf '\033[1;36m▸\033[0m %s\n' "$*"; }
|
|
||||||
warn(){ printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
|
|
||||||
die(){ printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }
|
|
||||||
|
|
||||||
# ── resolve source ──
|
|
||||||
MODE="host"; HOST="$DEFAULT_HOST"; SRC_URL=""
|
|
||||||
case "${1:-}" in
|
|
||||||
--release) MODE="release"; SRC_URL="$RELEASE_URL" ;;
|
|
||||||
--local) MODE="local" ;;
|
|
||||||
"") SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
|
|
||||||
-*) die "unknown flag: $1 (use --release | --local | <host>)" ;;
|
|
||||||
*) HOST="$1"; SRC_URL="https://${HOST}/wg/toolbox.xpi" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# ── find a Firefox binary ──
|
|
||||||
FX=""
|
|
||||||
for c in firefox firefox-esr firefox-bin firefox-developer-edition; do
|
|
||||||
if command -v "$c" >/dev/null 2>&1; then FX="$c"; break; fi
|
|
||||||
done
|
|
||||||
if [ -z "$FX" ] && command -v flatpak >/dev/null 2>&1 \
|
|
||||||
&& flatpak info org.mozilla.firefox >/dev/null 2>&1; then
|
|
||||||
FX="flatpak run org.mozilla.firefox"
|
|
||||||
fi
|
|
||||||
[ -n "$FX" ] || die "no Firefox found (install firefox / firefox-esr, or flatpak org.mozilla.firefox)"
|
|
||||||
say "Firefox: $FX"
|
|
||||||
|
|
||||||
have_webext(){ command -v web-ext >/dev/null 2>&1 || command -v npx >/dev/null 2>&1; }
|
|
||||||
runwebext(){ if command -v web-ext >/dev/null 2>&1; then web-ext "$@"; else npx --yes web-ext "$@"; fi; }
|
|
||||||
|
|
||||||
# ── fastest path: web-ext run (temporary load, no signing needed) ──
|
|
||||||
if have_webext; then
|
|
||||||
SRCDIR=""
|
|
||||||
if [ "$MODE" = "local" ]; then
|
|
||||||
[ -f "$SELF_DIR/manifest.json" ] || die "--local: no manifest.json next to this script"
|
|
||||||
SRCDIR="$SELF_DIR"
|
|
||||||
else
|
|
||||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
||||||
say "Downloading extension from ${SRC_URL} …"
|
|
||||||
curl -fsSL "$SRC_URL" -o "$TMP/sbx.xpi" || die "download failed: $SRC_URL"
|
|
||||||
head -c2 "$TMP/sbx.xpi" | grep -q PK || die "not a valid .xpi (zip) — wrong host/URL?"
|
|
||||||
mkdir -p "$TMP/ext" && ( cd "$TMP/ext" && unzip -q "$TMP/sbx.xpi" )
|
|
||||||
SRCDIR="$TMP/ext"
|
|
||||||
fi
|
|
||||||
say "Launching Firefox with the ToolBoX extension loaded (temporary)…"
|
|
||||||
FXBIN="${FX%% *}" # web-ext wants the binary, not a flatpak wrapper
|
|
||||||
if [ "$FX" = "${FX# }" ] && command -v "$FXBIN" >/dev/null 2>&1; then
|
|
||||||
exec runwebext run --source-dir "$SRCDIR" --firefox "$FXBIN" \
|
|
||||||
--start-url "https://${HOST}/social/me"
|
|
||||||
fi
|
|
||||||
exec runwebext run --source-dir "$SRCDIR" --start-url "https://${HOST}/social/me"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── fallback: open the .xpi so Firefox shows the install prompt ──
|
|
||||||
warn "web-ext not found (no npx) — falling back to the install prompt."
|
|
||||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
||||||
[ "$MODE" = "local" ] && die "--local needs web-ext/npx; install nodejs or use a host/--release"
|
|
||||||
say "Downloading ${SRC_URL} …"
|
|
||||||
curl -fsSL "$SRC_URL" -o "$TMP/secubox-toolbox-webext.xpi" || die "download failed"
|
|
||||||
head -c2 "$TMP/secubox-toolbox-webext.xpi" | grep -q PK || die "not a valid .xpi"
|
|
||||||
cat <<'NOTE'
|
|
||||||
! The .xpi is unsigned. Stock Firefox release refuses a permanent install.
|
|
||||||
Use Firefox ESR/Developer/Nightly, or set in about:config:
|
|
||||||
xpinstall.signatures.required = false
|
|
||||||
…then accept the install prompt that opens now.
|
|
||||||
NOTE
|
|
||||||
say "Opening Firefox on the extension…"
|
|
||||||
exec $FX "$TMP/secubox-toolbox-webext.xpi"
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "SecuBox ToolBoX — Cartographie sociale",
|
|
||||||
"version": "0.1.1",
|
|
||||||
"description": "Surface the SecuBox R3 toolbox live tracker analysis (cartographie sociale) in your browser: live badge, per-session trackers, mini Round-Eye graph, RGPD wipe + PDF report.",
|
|
||||||
"browser_specific_settings": {
|
|
||||||
"gecko": {
|
|
||||||
"id": "secubox-toolbox-webext@cybermind.fr",
|
|
||||||
"strict_min_version": "115.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permissions": ["storage", "alarms"],
|
|
||||||
"host_permissions": [
|
|
||||||
"*://*.secubox.in/*"
|
|
||||||
],
|
|
||||||
"action": {
|
|
||||||
"default_title": "SecuBox Cartographie",
|
|
||||||
"default_popup": "popup/popup.html",
|
|
||||||
"default_icon": {
|
|
||||||
"48": "icons/icon-48.png",
|
|
||||||
"128": "icons/icon-128.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"icons": {
|
|
||||||
"48": "icons/icon-48.png",
|
|
||||||
"128": "icons/icon-128.png"
|
|
||||||
},
|
|
||||||
"background": {
|
|
||||||
"service_worker": "background.js",
|
|
||||||
"scripts": ["api.js", "background.js"]
|
|
||||||
},
|
|
||||||
"options_ui": {
|
|
||||||
"page": "options/options.html",
|
|
||||||
"open_in_tab": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>SecuBox Cartographie — Réglages</title>
|
|
||||||
<style>
|
|
||||||
body { background:#0a0a0f; color:#e8e6d9; font:14px/1.5 system-ui,sans-serif;
|
|
||||||
max-width:520px; margin:40px auto; padding:0 20px; }
|
|
||||||
h1 { color:#c9a84c; font-size:18px; }
|
|
||||||
label { display:block; color:#6b6b7a; font-size:12px; margin:14px 0 4px; }
|
|
||||||
input,select { width:100%; padding:8px; border-radius:6px; border:1px solid #333;
|
|
||||||
background:#14141c; color:#e8e6d9; }
|
|
||||||
button { margin-top:16px; padding:9px 14px; border-radius:6px; border:1px solid #c9a84c;
|
|
||||||
background:#c9a84c; color:#0a0a0f; font-weight:700; cursor:pointer; }
|
|
||||||
.muted { color:#6b6b7a; font-size:12px; }
|
|
||||||
#msg { color:#00ff41; min-height:18px; margin-top:10px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>👁️ SecuBox Cartographie — Réglages</h1>
|
|
||||||
<p class="muted">L'extension parle uniquement à ta cabine via le tunnel R3.
|
|
||||||
Aucune donnée n'est envoyée ailleurs.</p>
|
|
||||||
|
|
||||||
<label>Borne (hôte de la cabine)
|
|
||||||
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
|
|
||||||
</label>
|
|
||||||
<label>Fenêtre d'analyse
|
|
||||||
<select id="since">
|
|
||||||
<option value="3600">1 heure</option>
|
|
||||||
<option value="86400" selected>24 heures</option>
|
|
||||||
<option value="604800">7 jours</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Jeton de session (optionnel — sinon appairage auto via R3)
|
|
||||||
<input id="token" type="text" placeholder="mac.exp.nonce.sig" autocomplete="off">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button id="save">Enregistrer</button>
|
|
||||||
<p id="msg"></p>
|
|
||||||
|
|
||||||
<script src="../api.js"></script>
|
|
||||||
<script src="options.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
const api = globalThis.SbxApi;
|
|
||||||
const $ = (id) => document.getElementById(id);
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
const cfg = await api.getConfig();
|
|
||||||
$("host").value = cfg.host;
|
|
||||||
$("token").value = cfg.token || "";
|
|
||||||
$("since").value = String(cfg.since);
|
|
||||||
}
|
|
||||||
|
|
||||||
$("save").addEventListener("click", async () => {
|
|
||||||
await api.setConfig({
|
|
||||||
host: $("host").value.trim() || api.DEFAULTS.host,
|
|
||||||
token: $("token").value.trim(),
|
|
||||||
since: parseInt($("since").value, 10) || api.DEFAULTS.since,
|
|
||||||
});
|
|
||||||
api.ext.runtime.sendMessage({ type: "refresh" });
|
|
||||||
$("msg").textContent = "Enregistré ✓";
|
|
||||||
setTimeout(() => ($("msg").textContent = ""), 1500);
|
|
||||||
});
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
//
|
|
||||||
// Dependency-free mini "Round-Eye" cartographie : the device at the
|
|
||||||
// centre, top trackers on an outer ring, radius/colour by hits + tier.
|
|
||||||
// A compact stand-in for the full d3 view served at /social/{token}.
|
|
||||||
|
|
||||||
const SVGNS = "http://www.w3.org/2000/svg";
|
|
||||||
const PAL = {
|
|
||||||
base: "#c9a84c", // gold
|
|
||||||
cdn: "#00d4ff", // cyan
|
|
||||||
ab: "#e63946", // cinnabar (anti-bot)
|
|
||||||
opg: "#6e40c9", // void purple (operator-grade)
|
|
||||||
eye: "#00ff41", // matrix green
|
|
||||||
link: "#2a2a3a",
|
|
||||||
};
|
|
||||||
|
|
||||||
function el(name, attrs) {
|
|
||||||
const n = document.createElementNS(SVGNS, name);
|
|
||||||
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tierOf(node) {
|
|
||||||
if (node.opgrade_vendor) return "opg";
|
|
||||||
if (node.antibot_vendor) return "ab";
|
|
||||||
if (node.cdn_vendor) return "cdn";
|
|
||||||
return "base";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGraph(svg, data) {
|
|
||||||
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
|
||||||
const W = 260, H = 180, cx = W / 2, cy = H / 2;
|
|
||||||
|
|
||||||
const nodes = (data && data.nodes ? data.nodes.slice() : [])
|
|
||||||
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
|
|
||||||
.slice(0, 14);
|
|
||||||
|
|
||||||
if (!nodes.length) {
|
|
||||||
const t = el("text", { x: cx, y: cy, fill: "#6b6b7a", "font-size": 11,
|
|
||||||
"text-anchor": "middle" });
|
|
||||||
t.textContent = "Aucun traceur détecté pour l'instant";
|
|
||||||
svg.appendChild(t);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxHits = Math.max(...nodes.map((n) => n.hits || 1));
|
|
||||||
const R = 66;
|
|
||||||
|
|
||||||
// spokes first (under the dots)
|
|
||||||
nodes.forEach((n, i) => {
|
|
||||||
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
|
|
||||||
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
|
|
||||||
svg.appendChild(el("line", { x1: cx, y1: cy, x2: x, y2: y,
|
|
||||||
stroke: PAL.link, "stroke-width": 1 }));
|
|
||||||
});
|
|
||||||
|
|
||||||
// tracker dots
|
|
||||||
nodes.forEach((n, i) => {
|
|
||||||
const a = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
|
|
||||||
const x = cx + Math.cos(a) * R, y = cy + Math.sin(a) * R;
|
|
||||||
const r = 3 + Math.round(6 * Math.sqrt((n.hits || 1) / maxHits));
|
|
||||||
const fill = PAL[tierOf(n)];
|
|
||||||
const c = el("circle", { cx: x, cy: y, r, fill, "fill-opacity": 0.85 });
|
|
||||||
const title = el("title", {});
|
|
||||||
title.textContent = `${n.domain} — ${n.hits || 0} hits`
|
|
||||||
+ (n.cdn_vendor ? ` · ${n.cdn_vendor}` : "")
|
|
||||||
+ (n.antibot_vendor ? ` · anti-bot ${n.antibot_vendor}` : "")
|
|
||||||
+ (n.opgrade_vendor ? ` · opérateur ${n.opgrade_vendor}` : "");
|
|
||||||
c.appendChild(title);
|
|
||||||
svg.appendChild(c);
|
|
||||||
});
|
|
||||||
|
|
||||||
// the eye (device) at the centre
|
|
||||||
svg.appendChild(el("circle", { cx, cy, r: 13, fill: "#0c0c12",
|
|
||||||
stroke: PAL.eye, "stroke-width": 2 }));
|
|
||||||
svg.appendChild(el("circle", { cx, cy, r: 4.5, fill: PAL.eye }));
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis.renderGraph = renderGraph;
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
/* SPDX-License-Identifier: LicenseRef-CMSD-1.0 */
|
|
||||||
/* SecuBox cyberpunk/hermetic palette (DESIGN-CHARTER) */
|
|
||||||
:root {
|
|
||||||
--cosmos: #0a0a0f;
|
|
||||||
--gold: #c9a84c;
|
|
||||||
--cinnabar: #e63946;
|
|
||||||
--matrix: #00ff41;
|
|
||||||
--void: #6e40c9;
|
|
||||||
--cyan: #00d4ff;
|
|
||||||
--text: #e8e6d9;
|
|
||||||
--muted: #6b6b7a;
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
width: 300px;
|
|
||||||
margin: 0;
|
|
||||||
background: var(--cosmos);
|
|
||||||
color: var(--text);
|
|
||||||
font: 13px/1.4 system-ui, "Segoe UI", sans-serif;
|
|
||||||
padding: 10px 12px 8px;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.logo { color: var(--gold); font-weight: 700; letter-spacing: .5px; }
|
|
||||||
.r3 {
|
|
||||||
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
|
|
||||||
border: 1px solid currentColor;
|
|
||||||
}
|
|
||||||
.r3.on { color: var(--matrix); }
|
|
||||||
.r3.off { color: var(--muted); }
|
|
||||||
.muted { color: var(--muted); font-size: 11px; }
|
|
||||||
.err { color: var(--cinnabar); font-size: 11px; min-height: 14px; }
|
|
||||||
|
|
||||||
label { display: block; font-size: 11px; color: var(--muted); margin: 8px 0 4px; }
|
|
||||||
input[type=text] {
|
|
||||||
width: 100%; padding: 7px 8px; border-radius: 6px;
|
|
||||||
border: 1px solid #333; background: #14141c; color: var(--text);
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
cursor: pointer; border: 1px solid #333; border-radius: 6px;
|
|
||||||
background: #14141c; color: var(--text); padding: 7px 8px; font-size: 12px;
|
|
||||||
}
|
|
||||||
button:hover { border-color: var(--gold); }
|
|
||||||
button.go {
|
|
||||||
width: 100%; margin-top: 8px; background: var(--gold); color: var(--cosmos);
|
|
||||||
font-weight: 700; border-color: var(--gold);
|
|
||||||
}
|
|
||||||
button.danger { color: var(--cinnabar); border-color: var(--cinnabar); }
|
|
||||||
|
|
||||||
.stats { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; margin-bottom: 8px; }
|
|
||||||
.stat {
|
|
||||||
background: #12121a; border: 1px solid #222; border-radius: 6px;
|
|
||||||
padding: 6px 2px; text-align: center;
|
|
||||||
}
|
|
||||||
.stat b { display: block; font-size: 16px; color: var(--gold); }
|
|
||||||
.stat span { font-size: 9px; color: var(--muted); }
|
|
||||||
.stat.warn b { color: var(--cinnabar); }
|
|
||||||
.stat.opg b { color: var(--void); }
|
|
||||||
|
|
||||||
#graph { width: 100%; height: 180px; background: #0c0c12; border-radius: 8px; display: block; }
|
|
||||||
|
|
||||||
.toplist { margin: 8px 0; max-height: 132px; overflow-y: auto; }
|
|
||||||
.row {
|
|
||||||
display: flex; align-items: center; gap: 6px; padding: 3px 2px;
|
|
||||||
border-bottom: 1px solid #1a1a22; font-size: 11px;
|
|
||||||
}
|
|
||||||
.row .dom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.row .hits { color: var(--muted); }
|
|
||||||
.tier { font-size: 9px; padding: 1px 4px; border-radius: 3px; }
|
|
||||||
.tier.cdn { background: #1d2a33; color: var(--cyan); }
|
|
||||||
.tier.ab { background: #2a1416; color: var(--cinnabar); }
|
|
||||||
.tier.opg { background: #1e1430; color: var(--void); }
|
|
||||||
|
|
||||||
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 6px 0; }
|
|
||||||
.actions button:last-child { grid-column: 1 / 3; }
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
|
||||||
margin-top: 6px; padding-top: 6px; border-top: 1px solid #1a1a22;
|
|
||||||
}
|
|
||||||
footer a { color: var(--cyan); text-decoration: none; font-size: 11px; }
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="popup.css">
|
|
||||||
<title>SecuBox Cartographie</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<span class="logo">👁️ VILLAGE3B</span>
|
|
||||||
<span id="r3dot" class="r3 off" title="État du tunnel R3">R3</span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Pairing (shown when no token) -->
|
|
||||||
<section id="pair" hidden>
|
|
||||||
<p class="muted">Connecte-toi à la cabine pour voir qui t'observe.</p>
|
|
||||||
<label>Borne
|
|
||||||
<input id="host" type="text" placeholder="kbin.gk2.secubox.in" autocomplete="off">
|
|
||||||
</label>
|
|
||||||
<button id="pairBtn" class="go">Appairer (R3)</button>
|
|
||||||
<p id="pairMsg" class="err"></p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Live view (shown when paired) -->
|
|
||||||
<section id="live" hidden>
|
|
||||||
<div class="stats">
|
|
||||||
<div class="stat"><b id="sTrackers">–</b><span>traceurs</span></div>
|
|
||||||
<div class="stat"><b id="sSites">–</b><span>sites</span></div>
|
|
||||||
<div class="stat warn"><b id="sAntibot">–</b><span>anti-bot</span></div>
|
|
||||||
<div class="stat opg"><b id="sOpgrade">–</b><span>opérateur</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<svg id="graph" viewBox="0 0 260 180" role="img" aria-label="Mini cartographie"></svg>
|
|
||||||
|
|
||||||
<div class="toplist" id="topList"></div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button id="openFull">🗺️ Cartographie complète</button>
|
|
||||||
<button id="pdf">📄 Rapport PDF</button>
|
|
||||||
<button id="wipe" class="danger">🧹 Effacer mes données</button>
|
|
||||||
</div>
|
|
||||||
<p id="liveMsg" class="muted"></p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<a href="#" id="settings">Réglages</a>
|
|
||||||
<span class="muted" id="ver"></span>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="../api.js"></script>
|
|
||||||
<script src="graph.js"></script>
|
|
||||||
<script src="popup.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
//
|
|
||||||
// SecuBox-Deb :: webext-toolbox :: popup controller
|
|
||||||
|
|
||||||
// NB: api.js (loaded first in this page) already declares `const ext` in the
|
|
||||||
// shared script scope — re-declaring it here is a "redeclaration of const ext"
|
|
||||||
// SyntaxError that aborts popup.js. Use api.ext instead.
|
|
||||||
const api = globalThis.SbxApi;
|
|
||||||
const $ = (id) => document.getElementById(id);
|
|
||||||
|
|
||||||
function show(which) {
|
|
||||||
$("pair").hidden = which !== "pair";
|
|
||||||
$("live").hidden = which !== "live";
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillTopList(nodes) {
|
|
||||||
const list = $("topList");
|
|
||||||
list.innerHTML = "";
|
|
||||||
(nodes || [])
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => (b.hits || 0) - (a.hits || 0))
|
|
||||||
.slice(0, 12)
|
|
||||||
.forEach((n) => {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "row";
|
|
||||||
const dom = document.createElement("span");
|
|
||||||
dom.className = "dom";
|
|
||||||
dom.textContent = n.domain || n.id;
|
|
||||||
row.appendChild(dom);
|
|
||||||
if (n.opgrade_vendor) addTier(row, "opg", n.opgrade_vendor);
|
|
||||||
else if (n.antibot_vendor) addTier(row, "ab", n.antibot_vendor);
|
|
||||||
else if (n.cdn_vendor) addTier(row, "cdn", n.cdn_vendor);
|
|
||||||
const hits = document.createElement("span");
|
|
||||||
hits.className = "hits";
|
|
||||||
hits.textContent = (n.hits || 0) + "×";
|
|
||||||
row.appendChild(hits);
|
|
||||||
list.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function addTier(row, cls, label) {
|
|
||||||
const t = document.createElement("span");
|
|
||||||
t.className = "tier " + cls;
|
|
||||||
t.textContent = label;
|
|
||||||
row.appendChild(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
function paint(data) {
|
|
||||||
const s = data.stats || {};
|
|
||||||
$("sTrackers").textContent = s.total_trackers ?? 0;
|
|
||||||
$("sSites").textContent = s.total_sites ?? 0;
|
|
||||||
$("sAntibot").textContent = s.antibot_sites ?? 0;
|
|
||||||
$("sOpgrade").textContent = s.opgrade_sites ?? 0;
|
|
||||||
globalThis.renderGraph($("graph"), data);
|
|
||||||
fillTopList(data.nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
const cfg = await api.getConfig();
|
|
||||||
$("ver").textContent = "v" + (api.ext.runtime.getManifest().version || "");
|
|
||||||
|
|
||||||
// tunnel indicator
|
|
||||||
api.r3Check(cfg.host).then((r) => {
|
|
||||||
const dot = $("r3dot");
|
|
||||||
dot.className = "r3 " + (r.tunnel ? "on" : "off");
|
|
||||||
dot.title = r.tunnel ? `Tunnel R3 actif (${r.peer_ip || "?"})` : "Hors tunnel R3";
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!cfg.token) {
|
|
||||||
$("host").value = cfg.host;
|
|
||||||
show("pair");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
show("live");
|
|
||||||
$("liveMsg").textContent = "Chargement…";
|
|
||||||
try {
|
|
||||||
const data = await api.graph(cfg.host, cfg.token, cfg.since);
|
|
||||||
paint(data);
|
|
||||||
$("liveMsg").textContent = "";
|
|
||||||
} catch (e) {
|
|
||||||
if (String(e.message) === "token-expired") {
|
|
||||||
// token died — drop it and go back to pairing
|
|
||||||
await api.setConfig({ token: "" });
|
|
||||||
show("pair");
|
|
||||||
$("host").value = cfg.host;
|
|
||||||
$("pairMsg").textContent = "Session expirée — ré-appaire.";
|
|
||||||
} else {
|
|
||||||
$("liveMsg").textContent = "Erreur : " + e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── events ──
|
|
||||||
$("pairBtn").addEventListener("click", async () => {
|
|
||||||
const host = $("host").value.trim() || api.DEFAULTS.host;
|
|
||||||
$("pairMsg").textContent = "Appairage…";
|
|
||||||
try {
|
|
||||||
await api.setConfig({ host });
|
|
||||||
const token = await api.pair(host);
|
|
||||||
await api.setConfig({ token });
|
|
||||||
api.ext.runtime.sendMessage({ type: "refresh" });
|
|
||||||
await load();
|
|
||||||
} catch (e) {
|
|
||||||
$("pairMsg").textContent = e.message + " (es-tu sur le tunnel ?)";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("openFull").addEventListener("click", async () => {
|
|
||||||
const cfg = await api.getConfig();
|
|
||||||
api.ext.tabs.create({ url: api.socialUrl(cfg.host, cfg.token) });
|
|
||||||
});
|
|
||||||
$("pdf").addEventListener("click", async () => {
|
|
||||||
const cfg = await api.getConfig();
|
|
||||||
api.ext.tabs.create({ url: api.reportUrl(cfg.host, cfg.token) });
|
|
||||||
});
|
|
||||||
$("wipe").addEventListener("click", async () => {
|
|
||||||
if (!confirm("Effacer toutes tes données de cartographie sur la cabine ?")) return;
|
|
||||||
const cfg = await api.getConfig();
|
|
||||||
try {
|
|
||||||
const r = await api.wipe(cfg.host, cfg.token);
|
|
||||||
$("liveMsg").textContent = `Effacé : ${r.rows_deleted ?? 0} entrées.`;
|
|
||||||
await api.setConfig({ token: "" });
|
|
||||||
setTimeout(load, 800);
|
|
||||||
} catch (e) {
|
|
||||||
$("liveMsg").textContent = "Erreur effacement : " + e.message;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$("settings").addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
api.ext.runtime.openOptionsPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
"""Shared classifiers used by mitm-ingest enrich_hooks across modules.
|
|
||||||
|
|
||||||
- host_app : host/SNI → app + category + emoji
|
|
||||||
- cookie : cookie name → provider + category + emoji
|
|
||||||
- avatar : UA → device + browser + os + emoji
|
|
||||||
- ja4 : TLS ClientHello fingerprint hash
|
|
||||||
"""
|
|
||||||
from . import host_app, cookie, avatar, ja4 # noqa: F401
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
|
|
||||||
"""Avatar analysis : UA + Client Hints → device emoji + readable name."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Devices identification patterns. Order = priority (first match wins).
|
|
||||||
DEVICE_PATTERNS = [
|
|
||||||
# ── iPhone ──
|
|
||||||
(re.compile(r"iPhone\s?OS\s?(\d+_\d+)|iPhone.*OS\s?(\d+_\d+)", re.I),
|
|
||||||
"iPhone", "📱", "iPhone iOS {}"),
|
|
||||||
(re.compile(r"iPhone", re.I), "iPhone", "📱", "iPhone"),
|
|
||||||
# ── iPad ──
|
|
||||||
(re.compile(r"iPad", re.I), "iPad", "📱", "iPad"),
|
|
||||||
# ── Mac ──
|
|
||||||
(re.compile(r"Mac OS X (\d+[._]\d+)", re.I), "Mac", "💻", "macOS {}"),
|
|
||||||
(re.compile(r"Macintosh", re.I), "Mac", "💻", "Mac"),
|
|
||||||
# ── Android ──
|
|
||||||
(re.compile(r"Pixel\s?(\d+)", re.I), "Pixel", "📱", "Pixel {}"),
|
|
||||||
(re.compile(r"SM-[A-Z]\d+", re.I), "Samsung", "📱", "Samsung"),
|
|
||||||
(re.compile(r"Android (\d+)", re.I), "Android", "📱", "Android {}"),
|
|
||||||
(re.compile(r"Android", re.I), "Android", "📱", "Android"),
|
|
||||||
# ── Windows ──
|
|
||||||
(re.compile(r"Windows NT 11"), "Windows", "💻", "Windows 11"),
|
|
||||||
(re.compile(r"Windows NT 10"), "Windows", "💻", "Windows 10"),
|
|
||||||
(re.compile(r"Windows NT"), "Windows", "💻", "Windows"),
|
|
||||||
# ── Linux ──
|
|
||||||
(re.compile(r"Linux", re.I), "Linux", "🐧", "Linux"),
|
|
||||||
# ── Game / IoT ──
|
|
||||||
(re.compile(r"PlayStation", re.I), "PlayStation", "🎮", "PlayStation"),
|
|
||||||
(re.compile(r"Xbox", re.I), "Xbox", "🎮", "Xbox"),
|
|
||||||
(re.compile(r"Nintendo", re.I), "Nintendo", "🎮", "Nintendo"),
|
|
||||||
(re.compile(r"AppleTV", re.I), "Apple TV", "📺", "Apple TV"),
|
|
||||||
(re.compile(r"Roku", re.I), "Roku", "📺", "Roku"),
|
|
||||||
# ── Bot / known clients ──
|
|
||||||
(re.compile(r"curl/", re.I), "curl", "🛠", "curl"),
|
|
||||||
(re.compile(r"wget/", re.I), "wget", "🛠", "wget"),
|
|
||||||
]
|
|
||||||
|
|
||||||
BROWSER_PATTERNS = [
|
|
||||||
(re.compile(r"Edg/(\d+)"), "Edge", "🪟", "Edge {}"),
|
|
||||||
(re.compile(r"Chrome/(\d+)"), "Chrome", "🟢", "Chrome {}"),
|
|
||||||
(re.compile(r"Firefox/(\d+)"), "Firefox","🦊", "Firefox {}"),
|
|
||||||
(re.compile(r"Safari/(\d+)"), "Safari", "🧭", "Safari"),
|
|
||||||
(re.compile(r"OPR/(\d+)|Opera/(\d+)"), "Opera", "🔴", "Opera"),
|
|
||||||
(re.compile(r"DuckDuckGo/(\d+)"), "DuckDuckGo", "🦆", "DuckDuckGo {}"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def classify_user_agent(ua: str) -> dict:
|
|
||||||
"""Returns {device, device_emoji, os_label, browser, browser_emoji, browser_label, raw}."""
|
|
||||||
if not ua:
|
|
||||||
return {"device": "unknown", "device_emoji": "❔", "os_label": "?",
|
|
||||||
"browser": "unknown", "browser_emoji": "❔", "browser_label": "?",
|
|
||||||
"raw": ""}
|
|
||||||
device_match = None
|
|
||||||
device_label = "unknown"
|
|
||||||
for pattern, label, emoji, template in DEVICE_PATTERNS:
|
|
||||||
m = pattern.search(ua)
|
|
||||||
if m:
|
|
||||||
# Try to fill the template with first non-None group
|
|
||||||
groups = [g for g in m.groups() if g]
|
|
||||||
if groups and "{}" in template:
|
|
||||||
device_label = template.format(groups[0].replace("_", "."))
|
|
||||||
else:
|
|
||||||
device_label = template
|
|
||||||
device_match = {"device": label, "device_emoji": emoji,
|
|
||||||
"os_label": device_label}
|
|
||||||
break
|
|
||||||
if not device_match:
|
|
||||||
device_match = {"device": "unknown", "device_emoji": "❔",
|
|
||||||
"os_label": ua[:50]}
|
|
||||||
browser_match = None
|
|
||||||
for pattern, label, emoji, template in BROWSER_PATTERNS:
|
|
||||||
m = pattern.search(ua)
|
|
||||||
if m:
|
|
||||||
groups = [g for g in m.groups() if g]
|
|
||||||
if groups and "{}" in template:
|
|
||||||
bl = template.format(groups[0])
|
|
||||||
else:
|
|
||||||
bl = template
|
|
||||||
browser_match = {"browser": label, "browser_emoji": emoji, "browser_label": bl}
|
|
||||||
break
|
|
||||||
if not browser_match:
|
|
||||||
browser_match = {"browser": "unknown", "browser_emoji": "❔", "browser_label": "?"}
|
|
||||||
|
|
||||||
return {**device_match, **browser_match, "raw": ua[:200]}
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_user_agents(ua_set: set[str] | list[str]) -> dict:
|
|
||||||
"""Aggregate a set of UAs : returns {devices, browsers, most_common, raw_count}."""
|
|
||||||
if not ua_set:
|
|
||||||
return {"devices": {}, "browsers": {}, "most_common": None, "raw_count": 0}
|
|
||||||
devices: dict[str, dict] = {}
|
|
||||||
browsers: dict[str, dict] = {}
|
|
||||||
for ua in ua_set:
|
|
||||||
cls = classify_user_agent(ua)
|
|
||||||
d = cls["device"]
|
|
||||||
if d not in devices:
|
|
||||||
devices[d] = {"count": 0, "emoji": cls["device_emoji"], "os_label": cls["os_label"]}
|
|
||||||
devices[d]["count"] += 1
|
|
||||||
b = cls["browser"]
|
|
||||||
if b not in browsers:
|
|
||||||
browsers[b] = {"count": 0, "emoji": cls["browser_emoji"], "label": cls["browser_label"]}
|
|
||||||
browsers[b]["count"] += 1
|
|
||||||
# Most common device
|
|
||||||
most_common = max(devices.items(), key=lambda x: x[1]["count"])[0] if devices else None
|
|
||||||
return {
|
|
||||||
"devices": devices,
|
|
||||||
"browsers": browsers,
|
|
||||||
"most_common": most_common,
|
|
||||||
"most_common_emoji": devices[most_common]["emoji"] if most_common else "❔",
|
|
||||||
"raw_count": len(ua_set),
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
|
|
||||||
"""Cookie analysis : identify trackers + providers + categorize.
|
|
||||||
|
|
||||||
Phase 2a+ heuristic: pattern matching sur les noms de cookies bien connus,
|
|
||||||
mapping vers fournisseur + catégorie (analytics / advertising / social / etc.).
|
|
||||||
|
|
||||||
Database extensible — pour Phase 3 on chargera depuis cookiepedia ou EasyList.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Pattern → (provider, category, emoji)
|
|
||||||
COOKIE_PATTERNS = [
|
|
||||||
# ── Analytics ──
|
|
||||||
(re.compile(r"^_ga(_|$|t)"), "Google Analytics", "analytics", "📊"),
|
|
||||||
(re.compile(r"^_gid$"), "Google Analytics", "analytics", "📊"),
|
|
||||||
(re.compile(r"^_gat"), "Google Analytics", "analytics", "📊"),
|
|
||||||
(re.compile(r"^_gcl_au$"), "Google Ads conversion", "advertising", "💰"),
|
|
||||||
(re.compile(r"^_pk_(id|ses|cvar)"), "Matomo / Piwik", "analytics", "📊"),
|
|
||||||
(re.compile(r"^plausible_"), "Plausible", "analytics", "📊"),
|
|
||||||
(re.compile(r"^_mkto_trk$"), "Marketo", "analytics", "📊"),
|
|
||||||
(re.compile(r"^__hssc$|^__hstc$"), "HubSpot", "analytics", "📊"),
|
|
||||||
(re.compile(r"^mp_[a-z0-9]+_mixpanel"), "Mixpanel", "analytics", "📊"),
|
|
||||||
(re.compile(r"^amplitude_"), "Amplitude", "analytics", "📊"),
|
|
||||||
(re.compile(r"^optimizelyEndUserId$"), "Optimizely", "analytics", "📊"),
|
|
||||||
(re.compile(r"^_hjSession"), "Hotjar", "analytics", "📊"),
|
|
||||||
(re.compile(r"^_hjFirstSeen$"), "Hotjar", "analytics", "📊"),
|
|
||||||
(re.compile(r"^crisp-client/session/"), "Crisp Chat", "analytics", "💬"),
|
|
||||||
# ── Advertising / Tracking ──
|
|
||||||
(re.compile(r"^_fbp$|^fr$"), "Facebook Pixel", "advertising","🎯"),
|
|
||||||
(re.compile(r"^IDE$"), "Google DoubleClick", "advertising","🎯"),
|
|
||||||
(re.compile(r"^NID$"), "Google", "advertising","🎯"),
|
|
||||||
(re.compile(r"^DSID$"), "Google DoubleClick", "advertising","🎯"),
|
|
||||||
(re.compile(r"^uid$|^bcookie$|^lidc$"), "LinkedIn Insight", "advertising","💼"),
|
|
||||||
(re.compile(r"^MUID$|^_uetsid$|^_uetvid$"), "Microsoft Clarity / Bing Ads", "advertising", "🎯"),
|
|
||||||
(re.compile(r"^_pin_unauth$|^_pinterest_ct_"), "Pinterest", "advertising","📌"),
|
|
||||||
(re.compile(r"^tt_appInfo$|^tt_webid"), "TikTok", "advertising","🎵"),
|
|
||||||
(re.compile(r"^_ttp$"), "TikTok Pixel", "advertising","🎵"),
|
|
||||||
(re.compile(r"^ANID$"), "Google", "advertising","🎯"),
|
|
||||||
(re.compile(r"^__qca$"), "Quantcast", "advertising","🎯"),
|
|
||||||
(re.compile(r"^__gads$|^__gpi$"), "Google AdSense", "advertising","💰"),
|
|
||||||
(re.compile(r"^test_cookie$"), "Google", "advertising","🎯"),
|
|
||||||
# ── Social ──
|
|
||||||
(re.compile(r"^c_user$|^xs$|^datr$"), "Facebook", "social", "👥"),
|
|
||||||
(re.compile(r"^sb$|^locale$|^wd$"), "Facebook", "social", "👥"),
|
|
||||||
(re.compile(r"^twid$|^ct0$|^auth_token$"), "Twitter / X", "social", "👥"),
|
|
||||||
(re.compile(r"^li_at$"), "LinkedIn", "social", "👥"),
|
|
||||||
(re.compile(r"^IG_"), "Instagram", "social", "👥"),
|
|
||||||
# ── Auth / Session (legit, no tracker) ──
|
|
||||||
(re.compile(r"^session(_id)?$|^sessionid$"), "Session generic", "session", "🔑"),
|
|
||||||
(re.compile(r"^csrftoken$|^_csrf$"), "CSRF token", "session", "🔒"),
|
|
||||||
(re.compile(r"^XSRF-TOKEN$"), "XSRF token", "session", "🔒"),
|
|
||||||
(re.compile(r"^remember_token$"), "Remember-me", "session", "🔑"),
|
|
||||||
(re.compile(r"^PHPSESSID$"), "PHP session", "session", "🔑"),
|
|
||||||
(re.compile(r"^JSESSIONID$"), "Java session", "session", "🔑"),
|
|
||||||
(re.compile(r"^connect\.sid$"), "Express.js session", "session", "🔑"),
|
|
||||||
# ── CDN / infra ──
|
|
||||||
(re.compile(r"^__cf_bm$|^cf_clearance$"), "Cloudflare", "infra", "☁"),
|
|
||||||
(re.compile(r"^_dd_s$"), "Datadog RUM", "monitoring", "📈"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def classify_cookie_name(name: str) -> dict:
|
|
||||||
"""Returns {provider, category, emoji} for a single cookie name.
|
|
||||||
Unknown → {provider: 'unknown', category: 'other', emoji: '❔'}."""
|
|
||||||
for pattern, provider, category, emoji in COOKIE_PATTERNS:
|
|
||||||
if pattern.search(name):
|
|
||||||
return {"provider": provider, "category": category, "emoji": emoji}
|
|
||||||
return {"provider": "unknown", "category": "other", "emoji": "❔"}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_cookie_header(header_value: str) -> list[str]:
|
|
||||||
"""Parse 'Cookie:' or 'Set-Cookie:' value, return list of cookie NAMES."""
|
|
||||||
if not header_value:
|
|
||||||
return []
|
|
||||||
names = []
|
|
||||||
for part in header_value.split(";"):
|
|
||||||
if "=" in part:
|
|
||||||
n = part.split("=", 1)[0].strip()
|
|
||||||
if n:
|
|
||||||
names.append(n)
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_cookie_events(cookie_events: list[dict]) -> dict:
|
|
||||||
"""Aggregate cookie events into stats + per-provider breakdown.
|
|
||||||
|
|
||||||
Input : list of {url, set_cookie_count, cookie_count, ...} from local_store
|
|
||||||
(note : Phase 1.5 stored only counts, not names. Phase 2a+ local_store
|
|
||||||
should store names. Until then, this function works on whatever's present.)
|
|
||||||
|
|
||||||
Returns :
|
|
||||||
{
|
|
||||||
providers: {provider: {count, category, emoji}, ...},
|
|
||||||
categories: {category: count, ...},
|
|
||||||
unknown_count: int,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
providers: dict[str, dict] = {}
|
|
||||||
categories: dict[str, int] = {}
|
|
||||||
unknown_count = 0
|
|
||||||
|
|
||||||
for ev in cookie_events:
|
|
||||||
# The cookie name might be in `set_cookie_names` or `cookie_names` if Phase 2a+
|
|
||||||
# local_store. Backward-compat : skip if absent.
|
|
||||||
for key in ("set_cookie_names", "cookie_names"):
|
|
||||||
names = ev.get(key, [])
|
|
||||||
if not isinstance(names, list):
|
|
||||||
continue
|
|
||||||
for n in names:
|
|
||||||
cls = classify_cookie_name(n)
|
|
||||||
p = cls["provider"]
|
|
||||||
if p == "unknown":
|
|
||||||
unknown_count += 1
|
|
||||||
else:
|
|
||||||
if p not in providers:
|
|
||||||
providers[p] = {"count": 0, "category": cls["category"],
|
|
||||||
"emoji": cls["emoji"]}
|
|
||||||
providers[p]["count"] += 1
|
|
||||||
cat = cls["category"]
|
|
||||||
categories[cat] = categories.get(cat, 0) + 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
"providers": providers,
|
|
||||||
"categories": categories,
|
|
||||||
"unknown_count": unknown_count,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Quick lookup for live use in /report endpoints
|
|
||||||
def top_providers(cookie_events: list[dict], limit: int = 10) -> list[dict]:
|
|
||||||
"""Returns top providers by hit count : [{provider, count, category, emoji}, ...]"""
|
|
||||||
stats = analyze_cookie_events(cookie_events)
|
|
||||||
return sorted(
|
|
||||||
[{"provider": p, **v} for p, v in stats["providers"].items()],
|
|
||||||
key=lambda x: -x["count"],
|
|
||||||
)[:limit]
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
|
|
||||||
"""JA4 / JA4-like TLS ClientHello fingerprint.
|
|
||||||
|
|
||||||
Reference: https://github.com/FoxIO-LLC/ja4 (BSD-3)
|
|
||||||
|
|
||||||
Phase 2c implementation : compute a deterministic, JA4-style fingerprint
|
|
||||||
hash from cipher_suites + alpn_protocols + extensions. The output is
|
|
||||||
12-char hex (truncated SHA256), suitable for matching against external
|
|
||||||
JA4 databases (custom curation, not the full FoxIO format).
|
|
||||||
|
|
||||||
This is NOT the canonical FoxIO JA4 string. It's a deterministic
|
|
||||||
fingerprint that's stable per-client-stack, so the same iPhone Safari
|
|
||||||
will always yield the same hash. We can map known hashes to bots,
|
|
||||||
trackers, malware C2 in Phase 3.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
|
|
||||||
def _sort_norm(items: list | None) -> str:
|
|
||||||
"""Sort + join items as canonical comma-separated lowercase string."""
|
|
||||||
if not items:
|
|
||||||
return ""
|
|
||||||
parts = []
|
|
||||||
for x in items:
|
|
||||||
if isinstance(x, bytes):
|
|
||||||
parts.append(x.hex())
|
|
||||||
else:
|
|
||||||
parts.append(str(x).lower())
|
|
||||||
return ",".join(sorted(parts))
|
|
||||||
|
|
||||||
|
|
||||||
def compute_ja4_hash(
|
|
||||||
*,
|
|
||||||
sni: str | None = None,
|
|
||||||
alpn_protocols: list | None = None,
|
|
||||||
cipher_suites: list | None = None,
|
|
||||||
extensions: list | None = None,
|
|
||||||
transport: str = "t", # 't' for TCP, 'q' for QUIC
|
|
||||||
tls_version: str = "13", # 13 for TLS 1.3, 12 for TLS 1.2
|
|
||||||
) -> dict:
|
|
||||||
"""Compute a JA4-style fingerprint dict.
|
|
||||||
|
|
||||||
Returns {
|
|
||||||
fingerprint : 12-char hex hash,
|
|
||||||
transport : t/q,
|
|
||||||
tls_version : 13/12,
|
|
||||||
alpn_count : int,
|
|
||||||
cipher_count : int,
|
|
||||||
ext_count : int,
|
|
||||||
sni_present : bool,
|
|
||||||
raw_repr : compact str repr for debug,
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
alpn_str = _sort_norm(alpn_protocols)
|
|
||||||
cipher_str = _sort_norm(cipher_suites)
|
|
||||||
ext_str = _sort_norm(extensions)
|
|
||||||
raw = f"{transport}{tls_version}|alpn={alpn_str}|c={cipher_str}|x={ext_str}"
|
|
||||||
h = hashlib.sha256(raw.encode("utf-8", errors="ignore")).hexdigest()[:12]
|
|
||||||
return {
|
|
||||||
"fingerprint": h,
|
|
||||||
"transport": transport,
|
|
||||||
"tls_version": tls_version,
|
|
||||||
"alpn_count": len(alpn_protocols or []),
|
|
||||||
"cipher_count": len(cipher_suites or []),
|
|
||||||
"ext_count": len(extensions or []),
|
|
||||||
"sni_present": bool(sni),
|
|
||||||
"raw_repr": raw[:200],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Phase 3-ready : map known JA4 hashes to client tags. Empty for now.
|
|
||||||
KNOWN_JA4_FINGERPRINTS: dict[str, dict] = {
|
|
||||||
# "abc123def456": {"label": "iPhone Safari 17.x", "category": "browser", "trust": "high"},
|
|
||||||
# "deadbeef0000": {"label": "Tor Browser 14.x", "category": "browser-anon", "trust": "medium"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def lookup_ja4(fingerprint: str) -> dict | None:
|
|
||||||
"""Return known label for a fingerprint, or None if unknown."""
|
|
||||||
return KNOWN_JA4_FINGERPRINTS.get(fingerprint)
|
|
||||||
|
|
@ -26,43 +26,13 @@ from secubox_core.logger import get_logger
|
||||||
|
|
||||||
app = FastAPI(title="secubox-avatar", version="1.0.0", root_path="/api/v1/avatar")
|
app = FastAPI(title="secubox-avatar", version="1.0.0", root_path="/api/v1/avatar")
|
||||||
|
|
||||||
# Phase 2b/2c (#488/#490) : ingest mitm avatar events + UA/CH device classification
|
# Phase 2b (#488) : ingest mitm avatar fingerprint events from secubox-toolbox addon
|
||||||
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
||||||
from secubox_core.classifiers import avatar as _avatar_cls # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _avatar_enrich(event: dict) -> dict:
|
|
||||||
"""Phase 2c enrichment : UA + Client Hints -> {device, browser, os, emoji}."""
|
|
||||||
ua = event.get("user_agent") or ""
|
|
||||||
if not ua:
|
|
||||||
return event
|
|
||||||
cls = _avatar_cls.classify_user_agent(ua)
|
|
||||||
# Augment with Client Hints if present (more reliable than UA spoofing)
|
|
||||||
chints = event.get("client_hints") or {}
|
|
||||||
if "sec-ch-ua-platform" in chints:
|
|
||||||
cls["ch_platform"] = chints["sec-ch-ua-platform"].strip('"')
|
|
||||||
if "sec-ch-ua-model" in chints:
|
|
||||||
cls["ch_model"] = chints["sec-ch-ua-model"].strip('"')
|
|
||||||
event["enriched"] = {
|
|
||||||
"device": cls.get("device", "unknown"),
|
|
||||||
"device_emoji": cls.get("device_emoji", "❔"),
|
|
||||||
"os_label": cls.get("os_label", "?"),
|
|
||||||
"browser": cls.get("browser", "unknown"),
|
|
||||||
"browser_emoji": cls.get("browser_emoji", "❔"),
|
|
||||||
"browser_label": cls.get("browser_label", "?"),
|
|
||||||
"ch_platform": cls.get("ch_platform"),
|
|
||||||
"ch_model": cls.get("ch_model"),
|
|
||||||
"source": "secubox-avatar/classifier",
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
mount_ingest_routes(
|
mount_ingest_routes(
|
||||||
app,
|
app,
|
||||||
endpoint_path="/fingerprint",
|
endpoint_path="/fingerprint",
|
||||||
db_path="/var/lib/secubox/avatar/mitm-ingest.db",
|
db_path="/var/lib/secubox/avatar/mitm-ingest.db",
|
||||||
kind="avatar",
|
kind="avatar",
|
||||||
enrich_hook=_avatar_enrich,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
|
|
@ -28,45 +28,13 @@ except ImportError:
|
||||||
|
|
||||||
app = FastAPI(title="SecuBox Cookies API", version="1.0.0")
|
app = FastAPI(title="SecuBox Cookies API", version="1.0.0")
|
||||||
|
|
||||||
# Phase 2b/2c (#488/#490) : ingest mitm cookies events + provider classification
|
# Phase 2b (#488) : ingest mitm cookies events from secubox-toolbox addon
|
||||||
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
||||||
from secubox_core.classifiers import cookie as _cookie_cls # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _cookies_enrich(event: dict) -> dict:
|
|
||||||
"""Phase 2c enrichment : map cookie names -> {providers[], categories{}}."""
|
|
||||||
set_names = event.get("set_cookie_names", []) or []
|
|
||||||
sent_names = event.get("cookie_names", []) or []
|
|
||||||
all_names = list(set_names) + list(sent_names)
|
|
||||||
if not all_names:
|
|
||||||
return event
|
|
||||||
providers: dict[str, dict] = {}
|
|
||||||
categories: dict[str, int] = {}
|
|
||||||
for n in all_names:
|
|
||||||
cls = _cookie_cls.classify_cookie_name(n)
|
|
||||||
p = cls["provider"]
|
|
||||||
if p != "unknown":
|
|
||||||
if p not in providers:
|
|
||||||
providers[p] = {"count": 0, "category": cls["category"], "emoji": cls["emoji"]}
|
|
||||||
providers[p]["count"] += 1
|
|
||||||
cat = cls["category"]
|
|
||||||
categories[cat] = categories.get(cat, 0) + 1
|
|
||||||
event["enriched"] = {
|
|
||||||
"providers": providers,
|
|
||||||
"categories": categories,
|
|
||||||
"total_names": len(all_names),
|
|
||||||
"tracker_count": sum(v["count"] for v in providers.values()),
|
|
||||||
"source": "secubox-cookies/classifier",
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
mount_ingest_routes(
|
mount_ingest_routes(
|
||||||
app,
|
app,
|
||||||
endpoint_path="/inject",
|
endpoint_path="/inject",
|
||||||
db_path="/var/lib/secubox/cookies/mitm-ingest.db",
|
db_path="/var/lib/secubox/cookies/mitm-ingest.db",
|
||||||
kind="cookies",
|
kind="cookies",
|
||||||
enrich_hook=_cookies_enrich,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configuration paths
|
# Configuration paths
|
||||||
|
|
|
||||||
|
|
@ -23,36 +23,13 @@ import httpx
|
||||||
|
|
||||||
app = FastAPI(title="secubox-dpi", version="2.0.0", root_path="/api/v1/dpi")
|
app = FastAPI(title="secubox-dpi", version="2.0.0", root_path="/api/v1/dpi")
|
||||||
|
|
||||||
# Phase 2b/2c (#488/#490) : ingest mitm DPI events + nDPI-style classification
|
# Phase 2b (#488) : ingest mitm DPI events from secubox-toolbox addon
|
||||||
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
||||||
from secubox_core.classifiers import host_app as _host_app # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _dpi_enrich(event: dict) -> dict:
|
|
||||||
"""Phase 2c enrichment : classify host/SNI -> {app, category, emoji}.
|
|
||||||
|
|
||||||
Future Phase 3 : query nDPI/netifyd daemon socket for live classification.
|
|
||||||
"""
|
|
||||||
host = event.get("host") or event.get("sni") or ""
|
|
||||||
if not host:
|
|
||||||
return event
|
|
||||||
cls = _host_app.classify_host(host)
|
|
||||||
event["enriched"] = {
|
|
||||||
"app": cls["app"],
|
|
||||||
"category": cls["category"],
|
|
||||||
"emoji": cls["emoji"],
|
|
||||||
"source": "secubox-dpi/host_app",
|
|
||||||
"method": "pattern-match",
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
mount_ingest_routes(
|
mount_ingest_routes(
|
||||||
app,
|
app,
|
||||||
endpoint_path="/classify",
|
endpoint_path="/classify",
|
||||||
db_path="/var/lib/secubox/dpi/mitm-ingest.db",
|
db_path="/var/lib/secubox/dpi/mitm-ingest.db",
|
||||||
kind="dpi",
|
kind="dpi",
|
||||||
enrich_hook=_dpi_enrich,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ══════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
|
|
@ -34,42 +34,13 @@ P2P_SOCKET = "/run/secubox/p2p.sock"
|
||||||
|
|
||||||
app = FastAPI(title="SecuBox SOC", version="2.0.0")
|
app = FastAPI(title="SecuBox SOC", version="2.0.0")
|
||||||
|
|
||||||
# Phase 2b/2c (#488/#490) : ingest mitm SOC events + score aggregation
|
# Phase 2b (#488) : ingest mitm SOC indicator events from secubox-toolbox addon
|
||||||
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def _soc_enrich(event: dict) -> dict:
|
|
||||||
"""Phase 2c enrichment : sum indicator weights -> score band.
|
|
||||||
|
|
||||||
Future Phase 3 : query threat-intel feeds (CrowdSec/ThreatFox/etc.)
|
|
||||||
locally instead of just summing static weights.
|
|
||||||
"""
|
|
||||||
indicators = event.get("indicators") or []
|
|
||||||
if not indicators:
|
|
||||||
return event
|
|
||||||
total_weight = sum((i.get("weight") or 0) for i in indicators if isinstance(i, dict))
|
|
||||||
band = "low"
|
|
||||||
if total_weight >= 50:
|
|
||||||
band = "high"
|
|
||||||
elif total_weight >= 20:
|
|
||||||
band = "medium"
|
|
||||||
kinds = sorted({i.get("kind", "?") for i in indicators if isinstance(i, dict)})
|
|
||||||
event["enriched"] = {
|
|
||||||
"total_weight": total_weight,
|
|
||||||
"band": band,
|
|
||||||
"indicator_kinds": kinds,
|
|
||||||
"indicator_count": len(indicators),
|
|
||||||
"source": "secubox-soc/scoring",
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
mount_ingest_routes(
|
mount_ingest_routes(
|
||||||
app,
|
app,
|
||||||
endpoint_path="/event",
|
endpoint_path="/event",
|
||||||
db_path="/var/lib/secubox/soc/mitm-ingest.db",
|
db_path="/var/lib/secubox/soc/mitm-ingest.db",
|
||||||
kind="soc",
|
kind="soc",
|
||||||
enrich_hook=_soc_enrich,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Data directories
|
# Data directories
|
||||||
|
|
|
||||||
|
|
@ -54,39 +54,13 @@ QUEUE_FILE = DATA_DIR / "pending_rules.json"
|
||||||
app = FastAPI(title="SecuBox Threat Analyst", version="1.0.0")
|
app = FastAPI(title="SecuBox Threat Analyst", version="1.0.0")
|
||||||
logger = logging.getLogger("secubox.threat-analyst")
|
logger = logging.getLogger("secubox.threat-analyst")
|
||||||
|
|
||||||
# Phase 2b/2c (#488/#490) : ingest mitm JA4 events + compute fingerprint hash
|
# Phase 2b (#488) : ingest mitm JA4 clienthello events from secubox-toolbox addon
|
||||||
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
from secubox_core.mitm_ingest import mount_ingest_routes # noqa: E402
|
||||||
from secubox_core.classifiers import ja4 as _ja4_cls # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _ja4_enrich(event: dict) -> dict:
|
|
||||||
"""Phase 2c enrichment : compute JA4-style fingerprint + lookup known clients."""
|
|
||||||
ja4_hash = _ja4_cls.compute_ja4_hash(
|
|
||||||
sni=event.get("sni"),
|
|
||||||
alpn_protocols=event.get("alpn_protocols"),
|
|
||||||
cipher_suites=event.get("cipher_suites"),
|
|
||||||
extensions=event.get("extensions"),
|
|
||||||
)
|
|
||||||
known = _ja4_cls.lookup_ja4(ja4_hash["fingerprint"])
|
|
||||||
event["enriched"] = {
|
|
||||||
"ja4_fingerprint": ja4_hash["fingerprint"],
|
|
||||||
"ja4_raw_repr": ja4_hash["raw_repr"],
|
|
||||||
"cipher_count": ja4_hash["cipher_count"],
|
|
||||||
"alpn_count": ja4_hash["alpn_count"],
|
|
||||||
"ext_count": ja4_hash["ext_count"],
|
|
||||||
"sni_present": ja4_hash["sni_present"],
|
|
||||||
"known_client": known, # None if unknown, dict if matched
|
|
||||||
"source": "secubox-threat-analyst/ja4",
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
mount_ingest_routes(
|
mount_ingest_routes(
|
||||||
app,
|
app,
|
||||||
endpoint_path="/ja4",
|
endpoint_path="/ja4",
|
||||||
db_path="/var/lib/secubox/threat-analyst/mitm-ingest.db",
|
db_path="/var/lib/secubox/threat-analyst/mitm-ingest.db",
|
||||||
kind="ja4",
|
kind="ja4",
|
||||||
enrich_hook=_ja4_enrich,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,20 @@
|
||||||
{# SPDX-License-Identifier: LicenseRef-CMSD-1.0 #}
|
{# SPDX-License-Identifier: LicenseRef-CMSD-1.0 #}
|
||||||
{# Public landing page — kbin.gk2.secubox.in #}
|
{# Public landing page — kbin.gk2.secubox.in #}
|
||||||
{# Radical-simplify redesign (#543): animated hero + one CTA + install panel
|
|
||||||
up top ; everything else folded behind "En savoir plus". #}
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang=fr><head>
|
<html lang=fr><head>
|
||||||
<meta charset=UTF-8>
|
<meta charset=UTF-8>
|
||||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||||
<meta name=description content="VILLAGE3B — Cabine numérique Gondwana : diagnostic compromission iPhone/Android anonyme, gratuit, open source CMSD">
|
<meta name=description content="VILLAGE3B — Cabine numérique Gondwana : diagnostic compromission iPhone/Android anonyme, gratuit, open source CMSD">
|
||||||
<title>👁️ VILLAGE3B — Qui te piste ?</title>
|
<title>📡 VILLAGE3B — Cabine Numérique Gondwana</title>
|
||||||
<link rel=manifest href=/manifest.json>
|
<link rel=manifest href=/manifest.json>
|
||||||
<style>
|
<style>
|
||||||
:root{--bg:#0a0a0f;--bg2:#0e0e15;--phos:#00dd44;--phos-hot:#00ff55;--dim:#006622;--text:#e8e6d9;--purple:#9e76ff;--gold:#c9a84c;--amber:#ffb347;--red:#ff4466;--cyan:#00d4ff}
|
:root{--bg:#0a0a0f;--bg2:#0e0e15;--phos:#00dd44;--phos-hot:#00ff55;--dim:#006622;--text:#e8e6d9;--purple:#9e76ff;--gold:#c9a84c;--amber:#ffb347;--red:#ff4466}
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
body{font-family:'Courier New',Menlo,monospace;background:var(--bg);color:var(--text);line-height:1.55;padding-bottom:3rem}
|
body{font-family:'Courier New',Menlo,monospace;background:var(--bg);color:var(--text);line-height:1.55;padding-bottom:3rem}
|
||||||
a{color:var(--phos);text-decoration:none}
|
.hero{background:linear-gradient(135deg,#1a0a2e 0%,#0a0a0f 100%);padding:2.5rem 1.5rem;text-align:center;border-bottom:2px solid var(--phos)}
|
||||||
a:hover{text-decoration:underline}
|
.hero h1{font-size:2.4rem;color:var(--phos-hot);text-shadow:0 0 8px var(--phos);letter-spacing:0.08em}
|
||||||
|
.hero p.tag{color:var(--gold);font-size:1rem;margin-top:0.4rem;letter-spacing:0.08em}
|
||||||
/* ── HERO ── */
|
.hero p.sub{color:var(--dim);font-size:0.85rem;margin-top:0.6rem;max-width:600px;margin-left:auto;margin-right:auto}
|
||||||
.hero{position:relative;overflow:hidden;background:radial-gradient(120% 120% at 50% -10%,#221041 0%,#0a0a0f 60%);padding:3rem 1.5rem 2.4rem;text-align:center;border-bottom:2px solid var(--phos)}
|
|
||||||
.eye{font-size:3.4rem;line-height:1;display:inline-block;animation:gaze 5s ease-in-out infinite;filter:drop-shadow(0 0 14px rgba(0,255,85,0.55))}
|
|
||||||
@keyframes gaze{0%,100%{transform:translateX(0) scale(1)}25%{transform:translateX(-6px) scale(1.04)}60%{transform:translateX(7px) scale(1.04)}}
|
|
||||||
.hero h1{font-size:2.6rem;color:var(--phos-hot);text-shadow:0 0 10px var(--phos);letter-spacing:0.08em;margin-top:0.3rem}
|
|
||||||
.hero .punch{color:var(--text);font-size:1.25rem;margin-top:0.6rem;font-weight:bold}
|
|
||||||
.hero .punch b{color:var(--gold)}
|
|
||||||
.hero .sub{color:var(--dim);font-size:0.82rem;margin-top:0.5rem;max-width:560px;margin-left:auto;margin-right:auto}
|
|
||||||
/* floating tracker dots = "who's watching" */
|
|
||||||
.dots{position:absolute;inset:0;pointer-events:none;z-index:0}
|
|
||||||
.dots i{position:absolute;width:7px;height:7px;border-radius:50%;opacity:0.0;animation:float 7s ease-in-out infinite}
|
|
||||||
.dots i:nth-child(1){left:12%;top:30%;background:var(--cyan);animation-delay:.0s}
|
|
||||||
.dots i:nth-child(2){left:82%;top:24%;background:var(--amber);animation-delay:1.1s}
|
|
||||||
.dots i:nth-child(3){left:24%;top:68%;background:var(--red);animation-delay:2.3s}
|
|
||||||
.dots i:nth-child(4){left:70%;top:64%;background:var(--purple);animation-delay:.7s}
|
|
||||||
.dots i:nth-child(5){left:50%;top:14%;background:var(--cyan);animation-delay:3.0s}
|
|
||||||
.dots i:nth-child(6){left:90%;top:54%;background:var(--red);animation-delay:1.8s}
|
|
||||||
@keyframes float{0%{opacity:0;transform:translateY(8px) scale(.6)}30%{opacity:.85}70%{opacity:.7}100%{opacity:0;transform:translateY(-14px) scale(1.1)}}
|
|
||||||
.hero>*{position:relative;z-index:1}
|
|
||||||
|
|
||||||
/* ── big CTA row ── */
|
|
||||||
.ctas{margin-top:1.4rem;display:flex;gap:0.6rem;justify-content:center;flex-wrap:wrap}
|
|
||||||
.cta{display:inline-block;padding:0.85rem 1.6rem;font-weight:bold;border-radius:8px;font-size:1.02rem;text-shadow:none;transition:transform .12s,box-shadow .12s}
|
|
||||||
.cta:hover{text-decoration:none;transform:translateY(-2px)}
|
|
||||||
.cta.go{background:var(--phos);color:#0a0a0f;box-shadow:0 4px 18px rgba(0,221,68,0.4)}
|
|
||||||
.cta.go:hover{box-shadow:0 6px 24px rgba(0,221,68,0.6)}
|
|
||||||
.cta.alt{background:transparent;color:var(--purple);border:1px solid var(--purple)}
|
|
||||||
.cta.alt:hover{background:rgba(158,118,255,0.12)}
|
|
||||||
|
|
||||||
/* ── quicknav (trimmed) ── */
|
|
||||||
.quicknav{display:flex;flex-wrap:wrap;justify-content:center;gap:0.6rem;margin-top:1.4rem;max-width:620px;margin-left:auto;margin-right:auto}
|
|
||||||
.qi{display:flex;flex-direction:column;align-items:center;gap:4px;padding:0.5rem 0.4rem;min-width:74px;background:rgba(110,64,201,0.08);border:1px solid var(--purple);border-radius:8px;text-decoration:none;color:var(--text);transition:all 0.12s;font-family:inherit}
|
|
||||||
.qi:hover{background:rgba(110,64,201,0.22);transform:translateY(-2px);box-shadow:0 4px 14px rgba(158,118,255,0.35);text-decoration:none}
|
|
||||||
.qi-emoji{font-size:1.5rem;line-height:1}
|
|
||||||
.qi-label{font-size:0.62rem;letter-spacing:0.04em;color:var(--phos-hot);font-weight:bold;white-space:nowrap}
|
|
||||||
|
|
||||||
.container{max-width:1080px;margin:auto;padding:2rem 1.5rem}
|
.container{max-width:1080px;margin:auto;padding:2rem 1.5rem}
|
||||||
.section{margin-bottom:2.5rem}
|
.section{margin-bottom:2.5rem}
|
||||||
h2{color:var(--phos-hot);text-shadow:0 0 4px var(--phos);font-size:1.3rem;margin-bottom:0.8rem;border-bottom:1px solid var(--dim);padding-bottom:0.4rem;letter-spacing:0.04em}
|
h2{color:var(--phos-hot);text-shadow:0 0 4px var(--phos);font-size:1.3rem;margin-bottom:0.8rem;border-bottom:1px solid var(--dim);padding-bottom:0.4rem;letter-spacing:0.04em}
|
||||||
|
|
@ -80,54 +43,30 @@ svg.chart{width:100%;max-width:400px;height:auto}
|
||||||
.svg-bar{fill:var(--phos);transition:fill 0.3s}
|
.svg-bar{fill:var(--phos);transition:fill 0.3s}
|
||||||
.svg-bar.medium{fill:var(--amber)}
|
.svg-bar.medium{fill:var(--amber)}
|
||||||
.svg-bar.high{fill:var(--red)}
|
.svg-bar.high{fill:var(--red)}
|
||||||
|
.steps{counter-reset:step}
|
||||||
|
.steps li{counter-increment:step;list-style:none;padding:0.6rem 0 0.6rem 2.4rem;position:relative;font-size:0.9rem}
|
||||||
|
.steps li::before{content:counter(step);position:absolute;left:0;top:0.5rem;width:1.8rem;height:1.8rem;border-radius:50%;background:var(--phos);color:#0a0a0f;text-align:center;line-height:1.8rem;font-weight:bold;text-shadow:none}
|
||||||
code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;color:var(--phos-hot)}
|
code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;color:var(--phos-hot)}
|
||||||
.cta-sm{display:inline-block;background:var(--phos);color:#0a0a0f;padding:0.7rem 1.4rem;text-decoration:none;font-weight:bold;border-radius:4px;margin:0.5rem 0.3rem 0.5rem 0;text-shadow:none}
|
a{color:var(--phos);text-decoration:none}
|
||||||
.cta-sm.outline{background:transparent;color:var(--phos);border:1px solid var(--phos)}
|
a:hover{text-decoration:underline}
|
||||||
|
.cta{display:inline-block;background:var(--phos);color:#0a0a0f;padding:0.7rem 1.4rem;text-decoration:none;font-weight:bold;border-radius:4px;margin:0.5rem 0.3rem 0.5rem 0;text-shadow:none}
|
||||||
|
.cta.outline{background:transparent;color:var(--phos);border:1px solid var(--phos)}
|
||||||
|
.cta.purple{background:var(--purple);color:#0a0a0f}
|
||||||
.footer{text-align:center;font-size:0.78rem;color:var(--dim);padding:1.5rem;border-top:1px solid var(--dim);margin-top:2rem}
|
.footer{text-align:center;font-size:0.78rem;color:var(--dim);padding:1.5rem;border-top:1px solid var(--dim);margin-top:2rem}
|
||||||
.arch{font-family:monospace;font-size:0.75rem;color:var(--phos-hot);text-shadow:0 0 4px var(--phos);background:var(--bg2);padding:1rem;border:1px solid var(--dim);border-radius:4px;overflow-x:auto;white-space:pre;line-height:1.4}
|
.arch{font-family:monospace;font-size:0.75rem;color:var(--phos-hot);text-shadow:0 0 4px var(--phos);background:var(--bg2);padding:1rem;border:1px solid var(--dim);border-radius:4px;overflow-x:auto;white-space:pre;line-height:1.4}
|
||||||
|
.quicknav{display:flex;flex-wrap:wrap;justify-content:center;gap:0.7rem;margin-top:1.2rem;max-width:780px;margin-left:auto;margin-right:auto}
|
||||||
/* ── install panel (kept up top) ── */
|
.qi{display:flex;flex-direction:column;align-items:center;gap:4px;padding:0.6rem 0.4rem;min-width:78px;background:rgba(110,64,201,0.08);border:1px solid var(--purple);border-radius:8px;text-decoration:none;color:var(--text);transition:all 0.12s;font-family:inherit}
|
||||||
.install-panel{background:rgba(0,255,65,0.04);border:1px solid rgba(0,255,65,0.25);border-radius:6px;padding:0.6rem 0.9rem;margin:0.45rem 0;text-align:left}
|
.qi:hover{background:rgba(110,64,201,0.22);transform:translateY(-2px);box-shadow:0 4px 14px rgba(158,118,255,0.35);text-decoration:none}
|
||||||
.install-panel summary{cursor:pointer;font-size:0.95rem;color:var(--phos-hot);list-style:none;outline:none}
|
.qi-emoji{font-size:1.6rem;line-height:1}
|
||||||
.install-panel summary::-webkit-details-marker{display:none}
|
.qi-label{font-size:0.65rem;letter-spacing:0.04em;color:var(--phos-hot);font-weight:bold;white-space:nowrap}
|
||||||
.install-panel[open] summary{margin-bottom:0.6rem}
|
|
||||||
.install-panel .emoji{font-size:1.1rem;margin-right:0.3rem}
|
|
||||||
.install-panel ol{padding-left:1.1rem;line-height:1.5;font-size:0.85rem}
|
|
||||||
.install-panel .btn{display:inline-block;padding:0.45rem 0.75rem;margin:0.25rem 0.2rem 0.25rem 0;background:var(--purple);color:#fff;text-decoration:none;border-radius:5px;font-weight:bold;font-size:0.82rem}
|
|
||||||
.install-panel .btn.alt{background:transparent;border:1px solid var(--purple);color:var(--purple)}
|
|
||||||
.install-panel code{background:rgba(0,0,0,0.4);padding:0.1rem 0.35rem;border-radius:3px;font-size:0.8rem;color:var(--phos-hot)}
|
|
||||||
.install-panel .note{color:var(--dim);font-size:0.78rem;margin-top:0.6rem;border-left:2px solid var(--amber);padding-left:0.6rem}
|
|
||||||
.install-panel img{max-width:100%;border-radius:5px;margin:0.4rem 0}
|
|
||||||
.install-panel pre{background:rgba(0,0,0,0.4);padding:0.5rem 0.7rem;border-radius:4px;overflow-x:auto;font-size:0.78rem;margin:0.4rem 0}
|
|
||||||
|
|
||||||
/* ── "En savoir plus" fold ── */
|
|
||||||
.more{max-width:1080px;margin:0 auto;padding:0 1.5rem}
|
|
||||||
.more>summary{cursor:pointer;list-style:none;text-align:center;color:var(--purple);font-size:0.95rem;letter-spacing:0.05em;padding:0.9rem;border:1px dashed var(--purple);border-radius:8px;margin-bottom:1rem;transition:background .12s}
|
|
||||||
.more>summary::-webkit-details-marker{display:none}
|
|
||||||
.more>summary:hover{background:rgba(158,118,255,0.1)}
|
|
||||||
.more[open]>summary{margin-bottom:1.6rem}
|
|
||||||
.more>summary .chev{display:inline-block;transition:transform .2s}
|
|
||||||
.more[open]>summary .chev{transform:rotate(90deg)}
|
|
||||||
|
|
||||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
|
|
||||||
.v.tick{animation:flash 0.6s}
|
|
||||||
@keyframes flash{0%{color:var(--gold);transform:scale(1.15)}100%{color:var(--phos-hot);transform:scale(1)}}
|
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
|
|
||||||
<div class=hero>
|
<div class=hero>
|
||||||
<div class=dots><i></i><i></i><i></i><i></i><i></i><i></i></div>
|
<h1>📡 VILLAGE3B</h1>
|
||||||
<span class=eye>👁️</span>
|
<p class=tag>// CABINE NUMÉRIQUE GONDWANA · TOOLBOX</p>
|
||||||
<h1>VILLAGE3B</h1>
|
|
||||||
<p class=punch>Qui te piste ? <b>La cabine te le montre.</b></p>
|
|
||||||
<p class=sub>Diagnostic gratuit de compromission iPhone / Android · Anonyme · Open Source · CMSD-1.0</p>
|
<p class=sub>Diagnostic gratuit de compromission iPhone / Android · Anonyme · Open Source · CMSD-1.0</p>
|
||||||
|
|
||||||
<div class=ctas>
|
{# Phase 6.I : quick-access icon nav — one-tap to all key endpoints #}
|
||||||
<a href="/wg/r3-install" class="cta go">✨ Protège-moi (R3)</a>
|
|
||||||
<a href="/social/me" class="cta alt">🕸️ Qui me piste ?</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# trimmed quick-nav — CA iPhone / CA Android / QR profil moved into the
|
|
||||||
per-platform install panel below (#543) #}
|
|
||||||
<div class=quicknav>
|
<div class=quicknav>
|
||||||
<a href="/wg/r3-install" class=qi title="Installer R3 WireGuard">
|
<a href="/wg/r3-install" class=qi title="Installer R3 WireGuard">
|
||||||
<span class=qi-emoji>🌐</span><span class=qi-label>R3 Install</span>
|
<span class=qi-emoji>🌐</span><span class=qi-label>R3 Install</span>
|
||||||
|
|
@ -138,6 +77,15 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
|
||||||
<a href="/social/me" class=qi title="Cartographie sociale — qui me piste, où ?">
|
<a href="/social/me" class=qi title="Cartographie sociale — qui me piste, où ?">
|
||||||
<span class=qi-emoji>🕸️</span><span class=qi-label>Ma carto</span>
|
<span class=qi-emoji>🕸️</span><span class=qi-label>Ma carto</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/wg/ca.mobileconfig" class=qi title="CA R3 iPhone (.mobileconfig)">
|
||||||
|
<span class=qi-emoji>📲</span><span class=qi-label>CA iPhone</span>
|
||||||
|
</a>
|
||||||
|
<a href="/wg/ca.pem" class=qi title="CA R3 Android/PC (.pem)">
|
||||||
|
<span class=qi-emoji>🤖</span><span class=qi-label>CA Android</span>
|
||||||
|
</a>
|
||||||
|
<a href="/wg/qr.png" class=qi title="QR profil WireGuard">
|
||||||
|
<span class=qi-emoji>📱</span><span class=qi-label>QR profil</span>
|
||||||
|
</a>
|
||||||
<a href="https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install" class=qi title="Wiki R3 multi-OS">
|
<a href="https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install" class=qi title="Wiki R3 multi-OS">
|
||||||
<span class=qi-emoji>📖</span><span class=qi-label>Wiki</span>
|
<span class=qi-emoji>📖</span><span class=qi-label>Wiki</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -147,25 +95,7 @@ code{background:#222;padding:0.1rem 0.4rem;border-radius:2px;font-size:0.85rem;c
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ── Install : auto-detected platform panel, front and centre ── #}
|
|
||||||
<div class=container>
|
<div class=container>
|
||||||
<div class=section style="margin-bottom:1.5rem">
|
|
||||||
<h2>📥 Installe en 1 tap</h2>
|
|
||||||
<p style="font-size:0.85rem;color:var(--dim);margin-bottom:0.8rem">
|
|
||||||
On a détecté <code>{{ install_platform }}</code> — le panneau adapté est ouvert.
|
|
||||||
Le CA, le QR et le profil sont dedans. Autre appareil ? Déplie le bon panneau.
|
|
||||||
</p>
|
|
||||||
{{ install_panels | safe }}
|
|
||||||
<p style="margin-top:0.8rem;font-size:0.78rem;color:var(--dim)">
|
|
||||||
R3 marche hors-cabine (4G/5G, autre WiFi), couvre tout le HTTPS, et se révoque
|
|
||||||
à tout moment. Page standalone : <a href=/wg/onboard>/wg/onboard</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# ── Everything else, folded ── #}
|
|
||||||
<details class=more>
|
|
||||||
<summary><span class=chev>▸</span> En savoir plus — la cabine en détail, en chiffres, en open source</summary>
|
|
||||||
|
|
||||||
{# ── KPI live (auto-refresh 5s via /cumulative-stats.json) ── #}
|
{# ── KPI live (auto-refresh 5s via /cumulative-stats.json) ── #}
|
||||||
<div class=section>
|
<div class=section>
|
||||||
|
|
@ -311,6 +241,43 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ── Install : auto-detected platform panels (Phase 8.2 #500) ── #}
|
||||||
|
<div class=section>
|
||||||
|
<h2>📥 Installer R3 sur ton appareil</h2>
|
||||||
|
<p style="font-size:0.85rem;color:var(--dim);margin-bottom:0.8rem">
|
||||||
|
On a détecté <code>{{ install_platform }}</code> via ton navigateur — le panneau adapté
|
||||||
|
est ouvert en premier. Autre appareil ? Déplie le bon panneau ci-dessous.
|
||||||
|
</p>
|
||||||
|
<style>
|
||||||
|
.install-panel{background:rgba(0,255,65,0.04);border:1px solid rgba(0,255,65,0.25);
|
||||||
|
border-radius:6px;padding:0.6rem 0.9rem;margin:0.45rem 0}
|
||||||
|
.install-panel summary{cursor:pointer;font-size:0.95rem;color:var(--phos-peak,#00dd44);
|
||||||
|
list-style:none;outline:none}
|
||||||
|
.install-panel summary::-webkit-details-marker{display:none}
|
||||||
|
.install-panel[open] summary{margin-bottom:0.6rem}
|
||||||
|
.install-panel .emoji{font-size:1.1rem;margin-right:0.3rem}
|
||||||
|
.install-panel ol{padding-left:1.1rem;line-height:1.5;font-size:0.85rem}
|
||||||
|
.install-panel .btn{display:inline-block;padding:0.45rem 0.75rem;margin:0.25rem 0.2rem 0.25rem 0;
|
||||||
|
background:var(--purple,#6e40c9);color:#fff;text-decoration:none;border-radius:5px;
|
||||||
|
font-weight:bold;font-size:0.82rem}
|
||||||
|
.install-panel .btn.alt{background:transparent;border:1px solid var(--purple,#6e40c9);
|
||||||
|
color:var(--purple,#6e40c9)}
|
||||||
|
.install-panel code{background:rgba(0,0,0,0.4);padding:0.1rem 0.35rem;border-radius:3px;
|
||||||
|
font-size:0.8rem;color:var(--phos-peak,#00dd44)}
|
||||||
|
.install-panel .note{color:var(--dim,#888);font-size:0.78rem;margin-top:0.6rem;
|
||||||
|
border-left:2px solid var(--phos-hot,#ffb347);padding-left:0.6rem}
|
||||||
|
.install-panel img{max-width:100%;border-radius:5px;margin:0.4rem 0}
|
||||||
|
.install-panel pre{background:rgba(0,0,0,0.4);padding:0.5rem 0.7rem;border-radius:4px;
|
||||||
|
overflow-x:auto;font-size:0.78rem;margin:0.4rem 0}
|
||||||
|
</style>
|
||||||
|
{{ install_panels | safe }}
|
||||||
|
<p style="margin-top:0.8rem;font-size:0.78rem;color:var(--dim)">
|
||||||
|
Avantage R3 : marche hors-cabine (4G/5G, autre WiFi). Inclut tout le trafic (HTTPS).
|
||||||
|
Profil + CA bundlés. Le tunnel est révoquable à tout moment depuis Réglages.
|
||||||
|
Page équivalente standalone : <a href=/wg/onboard>/wg/onboard</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{# ── Open Source ── #}
|
{# ── Open Source ── #}
|
||||||
<div class=section>
|
<div class=section>
|
||||||
<h2>🔓 Open Source — CMSD-1.0</h2>
|
<h2>🔓 Open Source — CMSD-1.0</h2>
|
||||||
|
|
@ -319,8 +286,8 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
|
||||||
(audit citoyen possible, droits d'usage régis par licence CMSD). Pas de boîte noire.
|
(audit citoyen possible, droits d'usage régis par licence CMSD). Pas de boîte noire.
|
||||||
</p>
|
</p>
|
||||||
<div style="margin-top:0.6rem">
|
<div style="margin-top:0.6rem">
|
||||||
<a href="https://github.com/CyberMind-FR/secubox-deb" class=cta-sm>📂 Code source GitHub</a>
|
<a href="https://github.com/CyberMind-FR/secubox-deb" class=cta>📂 Code source GitHub</a>
|
||||||
<a href="https://github.com/CyberMind-FR/secubox-deb/blob/master/LICENCE-CMSD-1.0.md" class="cta-sm outline">📜 Licence CMSD-1.0</a>
|
<a href="https://github.com/CyberMind-FR/secubox-deb/blob/master/LICENCE-CMSD-1.0.md" class="cta outline">📜 Licence CMSD-1.0</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -347,7 +314,7 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
</div>
|
||||||
|
|
||||||
<div class=footer>
|
<div class=footer>
|
||||||
📡 Gondwana ToolBox · CyberMind / Gérald Kerma · Notre-Dame-du-Cruet (73130) · Savoie · France<br>
|
📡 Gondwana ToolBox · CyberMind / Gérald Kerma · Notre-Dame-du-Cruet (73130) · Savoie · France<br>
|
||||||
|
|
@ -355,8 +322,14 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
|
||||||
// DIY · Open Source · Open Audit
|
// DIY · Open Source · Open Audit
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
|
||||||
|
.v.tick{animation:flash 0.6s}
|
||||||
|
@keyframes flash{0%{color:var(--gold);transform:scale(1.15)}100%{color:var(--phos-hot);transform:scale(1)}}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ── Live KPI auto-refresh from /cumulative-stats.json (+ count-up on first paint) ──
|
// ── Live KPI auto-refresh from /cumulative-stats.json ──
|
||||||
(function(){
|
(function(){
|
||||||
function dig(o,path){
|
function dig(o,path){
|
||||||
var parts = path.split('.');
|
var parts = path.split('.');
|
||||||
|
|
@ -376,23 +349,6 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
|
||||||
var ss = String(d.getSeconds()).padStart(2,'0');
|
var ss = String(d.getSeconds()).padStart(2,'0');
|
||||||
return 'maj ' + hh+':'+mm+':'+ss;
|
return 'maj ' + hh+':'+mm+':'+ss;
|
||||||
}
|
}
|
||||||
// count-up: animate each KPI from 0 → its server-rendered value, once.
|
|
||||||
function countUp(el, target){
|
|
||||||
var start = 0, dur = 900, t0 = null;
|
|
||||||
function step(ts){
|
|
||||||
if (t0 === null) t0 = ts;
|
|
||||||
var p = Math.min(1, (ts - t0) / dur);
|
|
||||||
var eased = 1 - Math.pow(1 - p, 3);
|
|
||||||
el.textContent = Math.round(start + (target - start) * eased);
|
|
||||||
if (p < 1) requestAnimationFrame(step);
|
|
||||||
else el.textContent = target;
|
|
||||||
}
|
|
||||||
requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
document.querySelectorAll('.kpi .v[data-live]').forEach(function(el){
|
|
||||||
var n = parseInt(el.textContent.trim(), 10);
|
|
||||||
if (!isNaN(n) && n > 0) countUp(el, n);
|
|
||||||
});
|
|
||||||
function refresh(){
|
function refresh(){
|
||||||
fetch('/cumulative-stats.json', {cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
|
fetch('/cumulative-stats.json', {cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
|
||||||
document.querySelectorAll('[data-live]').forEach(function(el){
|
document.querySelectorAll('[data-live]').forEach(function(el){
|
||||||
|
|
@ -423,10 +379,16 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
|
||||||
var fp = document.getElementById('cert-fp-r3');
|
var fp = document.getElementById('cert-fp-r3');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
|
|
||||||
|
// Display the WG CA fingerprint (use ?ca=wg flag if endpoint supports it,
|
||||||
|
// else fallback to default ca fingerprint).
|
||||||
fetch('/ca/fingerprint').then(function(r){return r.json();}).then(function(d){
|
fetch('/ca/fingerprint').then(function(r){return r.json();}).then(function(d){
|
||||||
fp.textContent = d.sha1 || d.sha256 || '?';
|
fp.textContent = d.sha1 || d.sha256 || '?';
|
||||||
}).catch(function(){fp.textContent='?';});
|
}).catch(function(){fp.textContent='?';});
|
||||||
|
|
||||||
|
// Phase 6.H : 3-step probe :
|
||||||
|
// 1) Detect if user is in WG R3 tunnel (probe our internal-only endpoint)
|
||||||
|
// 2) Probe an external HTTPS (verifies mitm decrypt + CA trust)
|
||||||
|
// 3) Combine results into a clear verdict
|
||||||
function runProbe(){
|
function runProbe(){
|
||||||
emj.textContent = '⏳';
|
emj.textContent = '⏳';
|
||||||
txt.innerHTML = 'Test en cours… (1/2 détection tunnel)';
|
txt.innerHTML = 'Test en cours… (1/2 détection tunnel)';
|
||||||
|
|
@ -454,12 +416,31 @@ LXC toolbox-mitm-wg 10.100.0.62 R3 WireGuard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 7 (#498) — same-origin HTTPS R3 probe.
|
||||||
|
// The previous probe loaded http://10.99.0.1:8088/qr/splash.png as
|
||||||
|
// an Image. iOS Safari blocks mixed content (HTTP from an HTTPS
|
||||||
|
// page) so the request never fired and inWG always stayed false.
|
||||||
|
// /wg/r3-check returns { tunnel: bool } based on the X-R3-Peer /
|
||||||
|
// XFF headers mitm-wg sets via the inject_xff addon.
|
||||||
fetch('/wg/r3-check?t=' + Date.now(), {cache: 'no-store'})
|
fetch('/wg/r3-check?t=' + Date.now(), {cache: 'no-store'})
|
||||||
.then(function(r){ return r.ok ? r.json() : {tunnel:false}; })
|
.then(function(r){ return r.ok ? r.json() : {tunnel:false}; })
|
||||||
.then(function(d){
|
.then(function(d){
|
||||||
inWG = !!(d && d.tunnel);
|
inWG = !!(d && d.tunnel);
|
||||||
if (!inWG) { finalize(); return; }
|
if (!inWG) { finalize(); return; }
|
||||||
txt.innerHTML = 'Tunnel R3 détecté ✓ — test 2/2 cert mitm…';
|
txt.innerHTML = 'Tunnel R3 détecté ✓ — test 2/2 cert mitm…';
|
||||||
|
// Step 2 : probe an external HTTPS host that mitm-wg DOES
|
||||||
|
// intercept (i.e. not in ignore_hosts). gstatic / google /
|
||||||
|
// apple / fbcdn are whitelisted so they pass through with
|
||||||
|
// their real cert ; useless to test CA trust. duckduckgo
|
||||||
|
// isn't whitelisted, so the TLS handshake is performed by
|
||||||
|
// mitm-wg with the R3 CA — it succeeds only if the iPhone
|
||||||
|
// trusts that CA.
|
||||||
|
//
|
||||||
|
// fetch(no-cors) resolves on any successful TLS handshake
|
||||||
|
// regardless of HTTP status, and rejects on cert / network
|
||||||
|
// error — exactly the signal we want. Image.onload was
|
||||||
|
// ambiguous : a 204 No Content reply (no image data) also
|
||||||
|
// triggered onerror, making CA-trusted look like CA-untrusted.
|
||||||
var extDone = false;
|
var extDone = false;
|
||||||
fetch('https://duckduckgo.com/favicon.ico?t=' + Date.now(),
|
fetch('https://duckduckgo.com/favicon.ico?t=' + Date.now(),
|
||||||
{mode: 'no-cors', cache: 'no-store'})
|
{mode: 'no-cors', cache: 'no-store'})
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,3 @@
|
||||||
secubox-toolbox (2.6.17-1~bookworm1) bookworm; urgency=medium
|
|
||||||
|
|
||||||
* Social correlation: domain-rollup + history + target↔tracker (#549).
|
|
||||||
- fetch_graph() now returns three additive top-level keys (read-time,
|
|
||||||
no schema change, d3 contract untouched):
|
|
||||||
· by_domain — trackers rolled up under their registrable parent
|
|
||||||
(eTLD+1, e.g. all *.doubleclick.net → doubleclick.net) with
|
|
||||||
tracker_count / hits / sites / vendors ;
|
|
||||||
· targets — inverse map: per 1st-party site, the trackers +
|
|
||||||
parent domains watching it ;
|
|
||||||
· history — per-UTC-day timeline (hits / trackers / sites) from
|
|
||||||
the raw social_edges log over the window.
|
|
||||||
- stats gains total_domains. Added a local _registrable_domain eTLD+1
|
|
||||||
helper (mirrors the addon, no publicsuffix dep).
|
|
||||||
|
|
||||||
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 13:00:00 +0200
|
|
||||||
|
|
||||||
secubox-toolbox (2.6.16-1~bookworm1) bookworm; urgency=medium
|
|
||||||
|
|
||||||
* Injected banner neon-tube redesign (#545) — inject_banner.py.
|
|
||||||
- New _LEVEL_THEME map: R3 (and the planned R4) get a neon-tube look
|
|
||||||
(dark glass bar, glowing tube border via layered box-shadow + neon
|
|
||||||
text-shadow on the title) ; R2 keeps the original amber flat bar.
|
|
||||||
- _banner_html_dynamic() takes the level and themes both the
|
|
||||||
CSP-strict (JS-less) and JS (dismissible) variants ; all inline CSS,
|
|
||||||
no injected <style>/@keyframes, ASCII/NCR-clean for legacy charsets.
|
|
||||||
- R4 theme defined but inert until _client_level() returns 'r4'.
|
|
||||||
|
|
||||||
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 12:30:00 +0200
|
|
||||||
|
|
||||||
secubox-toolbox (2.6.15-1~bookworm1) bookworm; urgency=medium
|
|
||||||
|
|
||||||
* kbin landing radical-simplify redesign (#543) — conf/landing.html.j2.
|
|
||||||
- Animated hero (gazing 👁️ + floating tracker dots) + one big
|
|
||||||
"✨ Protège-moi (R3)" CTA + "🕸️ Qui me piste ?" secondary.
|
|
||||||
- Auto-detected install panel pulled up front ("📥 Installe en 1 tap").
|
|
||||||
- KPIs / cert-probe / pitch / R0-R3 levels / charts / architecture /
|
|
||||||
open-source / contact moved behind an "En savoir plus" <details> fold.
|
|
||||||
- Quick-nav trimmed: removed CA iPhone / CA Android / QR profil cards
|
|
||||||
(they live inside the per-platform install panel now) — kept R3
|
|
||||||
Install / Mon rapport / Ma carto / Wiki / Cabine.
|
|
||||||
- Count-up animation on the live KPIs on first paint. All Jinja
|
|
||||||
variables + the live-stats and cert-probe scripts preserved.
|
|
||||||
|
|
||||||
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 12:00:00 +0200
|
|
||||||
|
|
||||||
secubox-toolbox (2.6.14-1~bookworm1) bookworm; urgency=medium
|
|
||||||
|
|
||||||
* Serve the browser ToolBoX extension .xpi from the toolbox (#532).
|
|
||||||
- api.py GET /wg/toolbox.xpi : serves the local .xpi
|
|
||||||
(/var/lib/secubox/toolbox/webext/secubox-toolbox-webext.xpi) with
|
|
||||||
content-type application/x-xpinstall ; if absent, 302 → the
|
|
||||||
tag-pinned GitHub release asset (button never dead-ends). The
|
|
||||||
webext release is published make_latest:false so it does not steal
|
|
||||||
"latest" from the Android APK release.
|
|
||||||
- /wg/onboard : new "🧩 Extension navigateur (cartographie)" button
|
|
||||||
on both the inline + _install_panels variants.
|
|
||||||
- sbin/secubox-toolbox-fetch-xpi : pulls the release asset into the
|
|
||||||
serve path (best-effort, ZIP-magic sanity check).
|
|
||||||
- postinst : create the webext serve dir + best-effort first fetch.
|
|
||||||
- New client clients/webext-toolbox/ (MV3 Firefox/Chromium): live
|
|
||||||
tracker badge + popup mini Round-Eye graph over /social/* ;
|
|
||||||
build-webext.yml publishes the .xpi on webext-v* tags.
|
|
||||||
|
|
||||||
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 10:30:00 +0200
|
|
||||||
|
|
||||||
secubox-toolbox (2.6.13-1~bookworm1) bookworm; urgency=medium
|
secubox-toolbox (2.6.13-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Serve the Android ToolBox APK from the toolbox (#536, follow-up #531).
|
* Serve the Android ToolBox APK from the toolbox (#536, follow-up #531).
|
||||||
|
|
|
||||||
|
|
@ -52,13 +52,6 @@ case "$1" in
|
||||||
if [ -x /usr/sbin/secubox-toolbox-fetch-apk ]; then
|
if [ -x /usr/sbin/secubox-toolbox-fetch-apk ]; then
|
||||||
/usr/sbin/secubox-toolbox-fetch-apk 2>&1 | head -2 || true
|
/usr/sbin/secubox-toolbox-fetch-apk 2>&1 | head -2 || true
|
||||||
fi
|
fi
|
||||||
# #532 : browser extension serve dir + best-effort fetch of the
|
|
||||||
# latest release .xpi (so GET /wg/toolbox.xpi serves it locally).
|
|
||||||
# Non-blocking : falls back to redirecting to the public release.
|
|
||||||
install -d -m 0755 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox/webext
|
|
||||||
if [ -x /usr/sbin/secubox-toolbox-fetch-xpi ]; then
|
|
||||||
/usr/sbin/secubox-toolbox-fetch-xpi 2>&1 | head -2 || true
|
|
||||||
fi
|
|
||||||
# /var/log/secubox is a SHARED parent traversed by many service users
|
# /var/log/secubox is a SHARED parent traversed by many service users
|
||||||
# (the aggregator runs as `secubox` and reads waf-threats.log under
|
# (the aggregator runs as `secubox` and reads waf-threats.log under
|
||||||
# here). It MUST be 0755 — a 0750 owned by secubox-toolbox silently
|
# here). It MUST be 0755 — a 0750 owned by secubox-toolbox silently
|
||||||
|
|
|
||||||
|
|
@ -105,9 +105,6 @@ execute_after_dh_auto_install:
|
||||||
# #536 : Android APK fetch helper.
|
# #536 : Android APK fetch helper.
|
||||||
install -m 0755 sbin/secubox-toolbox-fetch-apk \
|
install -m 0755 sbin/secubox-toolbox-fetch-apk \
|
||||||
debian/secubox-toolbox/usr/sbin/
|
debian/secubox-toolbox/usr/sbin/
|
||||||
# #532 : browser extension .xpi fetch helper.
|
|
||||||
install -m 0755 sbin/secubox-toolbox-fetch-xpi \
|
|
||||||
debian/secubox-toolbox/usr/sbin/
|
|
||||||
install -m 0755 sbin/secubox-toolbox-wg-restore \
|
install -m 0755 sbin/secubox-toolbox-wg-restore \
|
||||||
debian/secubox-toolbox/usr/sbin/
|
debian/secubox-toolbox/usr/sbin/
|
||||||
install -m 0644 systemd/secubox-toolbox-wg-restore.service \
|
install -m 0644 systemd/secubox-toolbox-wg-restore.service \
|
||||||
|
|
|
||||||
|
|
@ -404,40 +404,8 @@ def _detect_csp_strict(flow: http.HTTPFlow) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Per-level visual theme (#545). R3 — and the planned R4 — get the
|
|
||||||
# neon-tube treatment (dark glass bar, glowing tube border + neon
|
|
||||||
# text-shadow). R2 keeps the original amber flat bar. All values are inline
|
|
||||||
# CSS only (no injected <style>/@keyframes) so it survives strict CSP and
|
|
||||||
# arbitrary third-party pages.
|
|
||||||
_LEVEL_THEME = {
|
|
||||||
"r2": {
|
|
||||||
"neon": False,
|
|
||||||
"bg": "linear-gradient(90deg,#ffb347 60%,#0a0a0f 100%)",
|
|
||||||
"fg": "#0a0a0f", "edge": "#C04E24", "accent": "#ffb347",
|
|
||||||
"glow": "", "link": "#0a5840", "chip": "rgba(0,0,0,0.1)",
|
|
||||||
},
|
|
||||||
"r3": {
|
|
||||||
"neon": True,
|
|
||||||
"bg": "rgba(8,8,14,0.95)",
|
|
||||||
"fg": "#e8e6d9", "edge": "#00d4ff", "accent": "#00d4ff",
|
|
||||||
"glow": "rgba(0,212,255,0.45)", "link": "#00d4ff",
|
|
||||||
"chip": "rgba(0,212,255,0.12)",
|
|
||||||
},
|
|
||||||
# planned (#545): R4 drops in with its own neon colour — inert until
|
|
||||||
# _client_level() can return 'r4'.
|
|
||||||
"r4": {
|
|
||||||
"neon": True,
|
|
||||||
"bg": "rgba(12,8,16,0.96)",
|
|
||||||
"fg": "#e8e6d9", "edge": "#ff3df0", "accent": "#ff3df0",
|
|
||||||
"glow": "rgba(255,61,240,0.45)", "link": "#ff3df0",
|
|
||||||
"chip": "rgba(255,61,240,0.12)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
||||||
report_url: str, level_label: str,
|
report_url: str, level_label: str) -> bytes:
|
||||||
level: str = "r2") -> bytes:
|
|
||||||
"""Render the injection payload.
|
"""Render the injection payload.
|
||||||
|
|
||||||
Two flavors depending on CSP strictness :
|
Two flavors depending on CSP strictness :
|
||||||
|
|
@ -487,47 +455,22 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
||||||
# Static emojis used in the left-side text
|
# Static emojis used in the left-side text
|
||||||
SAT_EMOJI = "📡" # 📡 satellite dish
|
SAT_EMOJI = "📡" # 📡 satellite dish
|
||||||
|
|
||||||
# ── theme resolution (#545) : R3/R4 neon tube, R2 amber flat ──
|
|
||||||
th = _LEVEL_THEME.get(level, _LEVEL_THEME["r2"])
|
|
||||||
_base = (
|
|
||||||
"position:fixed!important;top:0!important;left:0!important;right:0!important;"
|
|
||||||
"z-index:2147483647!important;font-family:Menlo,Consolas,monospace!important;"
|
|
||||||
"padding:6px 12px!important;font-size:11px!important;line-height:1.4!important;"
|
|
||||||
"text-align:left!important;display:flex!important;justify-content:space-between!important;"
|
|
||||||
"align-items:center!important;gap:8px!important;"
|
|
||||||
)
|
|
||||||
if th["neon"]:
|
|
||||||
# glowing glass tube : outer + inset accent glow, neon edge
|
|
||||||
bar_css = (
|
|
||||||
_base
|
|
||||||
+ f"background:{th['bg']}!important;color:{th['fg']}!important;"
|
|
||||||
+ f"border-bottom:2px solid {th['edge']}!important;"
|
|
||||||
+ f"box-shadow:0 0 10px {th['accent']},0 3px 22px {th['glow']},"
|
|
||||||
f"inset 0 -1px 6px {th['glow']}!important;"
|
|
||||||
+ "backdrop-filter:blur(3px)!important;"
|
|
||||||
)
|
|
||||||
title_css = f"color:{th['accent']};text-shadow:0 0 6px {th['accent']},0 0 12px {th['accent']}"
|
|
||||||
else:
|
|
||||||
bar_css = (
|
|
||||||
_base
|
|
||||||
+ f"background:{th['bg']}!important;color:{th['fg']}!important;"
|
|
||||||
+ f"border-bottom:2px solid {th['edge']}!important;"
|
|
||||||
+ "box-shadow:0 2px 8px rgba(0,0,0,0.3)!important;"
|
|
||||||
)
|
|
||||||
title_css = ""
|
|
||||||
code_css = f"background:{th['chip']};padding:1px 4px;border-radius:2px"
|
|
||||||
link_css = f"color:{th['link']};text-decoration:underline;font-weight:bold"
|
|
||||||
title_attr = f" style=\"{title_css}\"" if title_css else ""
|
|
||||||
|
|
||||||
if csp_strict:
|
if csp_strict:
|
||||||
# JS-less HTML banner — visible only, no close button. !important
|
# JS-less HTML banner — visible only, no close button. !important
|
||||||
# everywhere so page CSS can't override the fixed positioning.
|
# everywhere so page CSS can't override the fixed positioning.
|
||||||
# NCRs work even when page charset is iso-8859-1.
|
# NCRs work even when page charset is iso-8859-1.
|
||||||
html = (
|
html = (
|
||||||
f"<div id=\"gondwana-mitm-banner\" role=\"status\" style=\"{bar_css}\">"
|
f"<div id=\"gondwana-mitm-banner\" role=\"status\" "
|
||||||
f"<span><b{title_attr}>{SAT_EMOJI} ToolBoX {level_label}</b> · CA SHA1: "
|
f"style=\"position:fixed!important;top:0!important;left:0!important;right:0!important;"
|
||||||
f"<code style=\"{code_css}\">{sha1[:23]}</code>"
|
f"z-index:2147483647!important;"
|
||||||
f" · <a href=\"{report_url}\" style=\"{link_css}\">Mon rapport</a></span>"
|
f"background:linear-gradient(90deg,#ffb347 60%,#0a0a0f 100%)!important;"
|
||||||
|
f"color:#0a0a0f!important;font-family:Menlo,Consolas,monospace!important;"
|
||||||
|
f"padding:6px 12px!important;font-size:11px!important;line-height:1.4!important;"
|
||||||
|
f"border-bottom:2px solid #C04E24!important;text-align:left!important;"
|
||||||
|
f"display:flex!important;justify-content:space-between!important;align-items:center!important;gap:8px!important\">"
|
||||||
|
f"<span><b>{SAT_EMOJI} ToolBoX {level_label}</b> · CA SHA1: "
|
||||||
|
f"<code style=\"background:rgba(0,0,0,0.1);padding:1px 4px;border-radius:2px\">{sha1[:23]}</code>"
|
||||||
|
f" · <a href=\"{report_url}\" style=\"color:#0a5840;text-decoration:underline;font-weight:bold\">Mon rapport</a></span>"
|
||||||
f"<span style=\"color:#e8e6d9;background:rgba(0,0,0,0.4);padding:3px 8px;border-radius:3px\">"
|
f"<span style=\"color:#e8e6d9;background:rgba(0,0,0,0.4);padding:3px 8px;border-radius:3px\">"
|
||||||
f"{right_text}"
|
f"{right_text}"
|
||||||
f" · <b style=\"color:{grade_color};background:#0a0a0f;padding:1px 5px;border-radius:2px\">{grade}</b>"
|
f" · <b style=\"color:{grade_color};background:#0a0a0f;padding:1px 5px;border-radius:2px\">{grade}</b>"
|
||||||
|
|
@ -546,12 +489,6 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
||||||
level_js = _json.dumps(level_label)
|
level_js = _json.dumps(level_label)
|
||||||
sat_js = _json.dumps(SAT_EMOJI)
|
sat_js = _json.dumps(SAT_EMOJI)
|
||||||
mid_js = _json.dumps(" · ")
|
mid_js = _json.dumps(" · ")
|
||||||
# theme (#545) — JS-encoded so the same neon/amber styling applies here
|
|
||||||
bar_css_js = _json.dumps(bar_css)
|
|
||||||
title_attr_js = _json.dumps(title_attr)
|
|
||||||
code_css_js = _json.dumps(code_css)
|
|
||||||
link_css_js = _json.dumps(link_css)
|
|
||||||
close_col_js = _json.dumps(th["fg"])
|
|
||||||
|
|
||||||
js = f"""
|
js = f"""
|
||||||
(function(){{
|
(function(){{
|
||||||
|
|
@ -562,7 +499,14 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
||||||
var b=document.createElement('div');
|
var b=document.createElement('div');
|
||||||
b.id='gondwana-mitm-banner';
|
b.id='gondwana-mitm-banner';
|
||||||
b.setAttribute('role','status');
|
b.setAttribute('role','status');
|
||||||
b.style.cssText={bar_css_js};
|
b.style.cssText='position:fixed!important;top:0!important;left:0!important;right:0!important;'+
|
||||||
|
'z-index:2147483647!important;'+
|
||||||
|
'background:linear-gradient(90deg,#ffb347 60%,#0a0a0f 100%)!important;'+
|
||||||
|
'color:#0a0a0f!important;font-family:Menlo,Consolas,monospace!important;'+
|
||||||
|
'padding:6px 12px!important;font-size:11px!important;line-height:1.4!important;'+
|
||||||
|
'border-bottom:2px solid #C04E24!important;box-shadow:0 2px 8px rgba(0,0,0,0.3)!important;'+
|
||||||
|
'text-align:left!important;display:flex!important;'+
|
||||||
|
'justify-content:space-between!important;align-items:center!important;gap:8px!important';
|
||||||
var rightText={right_js};
|
var rightText={right_js};
|
||||||
var grade={grade_js};
|
var grade={grade_js};
|
||||||
var gradeCol={grade_col_js};
|
var gradeCol={grade_col_js};
|
||||||
|
|
@ -571,18 +515,14 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
|
||||||
var level={level_js};
|
var level={level_js};
|
||||||
var SAT={sat_js};
|
var SAT={sat_js};
|
||||||
var MID={mid_js};
|
var MID={mid_js};
|
||||||
var TITLE_ATTR={title_attr_js};
|
b.innerHTML='<span><b>'+SAT+' ToolBoX '+level+'</b>'+MID+'CA SHA1: '+
|
||||||
var CODE_CSS={code_css_js};
|
'<code style=\"background:rgba(0,0,0,0.1);padding:1px 4px;border-radius:2px\">'+sha1+'</code>'+
|
||||||
var LINK_CSS={link_css_js};
|
MID+'<a href=\"'+reportUrl+'\" style=\"color:#0a5840;text-decoration:underline;font-weight:bold\">Mon rapport</a></span>'+
|
||||||
var CLOSE_COL={close_col_js};
|
|
||||||
b.innerHTML='<span><b'+TITLE_ATTR+'>'+SAT+' ToolBoX '+level+'</b>'+MID+'CA SHA1: '+
|
|
||||||
'<code style=\"'+CODE_CSS+'\">'+sha1+'</code>'+
|
|
||||||
MID+'<a href=\"'+reportUrl+'\" style=\"'+LINK_CSS+'\">Mon rapport</a></span>'+
|
|
||||||
'<span style=\"display:flex;align-items:center;gap:8px\">'+
|
'<span style=\"display:flex;align-items:center;gap:8px\">'+
|
||||||
'<span style=\"color:#e8e6d9;background:rgba(0,0,0,0.4);padding:3px 8px;border-radius:3px\">'+
|
'<span style=\"color:#e8e6d9;background:rgba(0,0,0,0.4);padding:3px 8px;border-radius:3px\">'+
|
||||||
rightText+MID+'<b style=\"color:'+gradeCol+';background:#0a0a0f;padding:1px 5px;border-radius:2px\">'+grade+'</b>'+
|
rightText+MID+'<b style=\"color:'+gradeCol+';background:#0a0a0f;padding:1px 5px;border-radius:2px\">'+grade+'</b>'+
|
||||||
'</span>'+
|
'</span>'+
|
||||||
'<a href=\"javascript:void(0)\" onclick=\"document.getElementById(\\'gondwana-mitm-banner\\').style.display=\\'none\\';document.body.style.paddingTop=0\" style=\"color:'+CLOSE_COL+';text-decoration:none;font-weight:bold;cursor:pointer\">[×]</a>'+
|
'<a href=\"javascript:void(0)\" onclick=\"document.getElementById(\\'gondwana-mitm-banner\\').style.display=\\'none\\';document.body.style.paddingTop=0\" style=\"color:#0a0a0f;text-decoration:none;font-weight:bold;cursor:pointer\">[×]</a>'+
|
||||||
'</span>';
|
'</span>';
|
||||||
if(document.body.firstChild){{document.body.insertBefore(b,document.body.firstChild)}}
|
if(document.body.firstChild){{document.body.insertBefore(b,document.body.firstChild)}}
|
||||||
else{{document.body.appendChild(b)}}
|
else{{document.body.appendChild(b)}}
|
||||||
|
|
@ -670,9 +610,8 @@ class InjectBanner:
|
||||||
csp_strict = _detect_csp_strict(flow)
|
csp_strict = _detect_csp_strict(flow)
|
||||||
report_url = _report_url_for(flow)
|
report_url = _report_url_for(flow)
|
||||||
level_label = _level_label(flow)
|
level_label = _level_label(flow)
|
||||||
level = _client_level(flow)
|
|
||||||
snippet = _banner_html_dynamic(_CA_SHA1, ctx, csp_strict,
|
snippet = _banner_html_dynamic(_CA_SHA1, ctx, csp_strict,
|
||||||
report_url, level_label, level)
|
report_url, level_label)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("banner compute failed for %s: %s", flow.request.host, e)
|
log.warning("banner compute failed for %s: %s", flow.request.host, e)
|
||||||
# Fail-open : skip injection rather than break the page
|
# Fail-open : skip injection rather than break the page
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
|
||||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
|
||||||
#
|
|
||||||
# SecuBox-Deb :: secubox-toolbox-fetch-xpi (#532)
|
|
||||||
#
|
|
||||||
# Pull the latest browser ToolBoX extension .xpi (published as a GitHub
|
|
||||||
# release asset by build-webext.yml on webext-v* tags) into the toolbox
|
|
||||||
# serve path, so GET /wg/toolbox.xpi serves it locally (offline-capable
|
|
||||||
# install from the cabine). Best-effort : a failure leaves any existing
|
|
||||||
# .xpi in place ; the endpoint falls back to the public release redirect.
|
|
||||||
set -euo pipefail
|
|
||||||
readonly MODULE="secubox-toolbox-fetch-xpi"
|
|
||||||
|
|
||||||
DEST_DIR="/var/lib/secubox/toolbox/webext"
|
|
||||||
DEST="${DEST_DIR}/secubox-toolbox-webext.xpi"
|
|
||||||
# Tag-pinned (not /latest/): the webext release is make_latest:false so it
|
|
||||||
# doesn't steal "latest" from the Android APK release. Bump on new webext-v*.
|
|
||||||
RELEASE_URL="https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.1/secubox-toolbox-webext.xpi"
|
|
||||||
|
|
||||||
log() { logger -t "$MODULE" -- "$*" 2>/dev/null || echo "[$MODULE] $*" >&2; }
|
|
||||||
|
|
||||||
install -d -m 0755 -o secubox-toolbox -g secubox-toolbox "$DEST_DIR" 2>/dev/null \
|
|
||||||
|| mkdir -p "$DEST_DIR"
|
|
||||||
|
|
||||||
TMP=$(mktemp --suffix=.xpi)
|
|
||||||
trap 'rm -f "$TMP"' EXIT
|
|
||||||
|
|
||||||
if command -v wget >/dev/null 2>&1; then
|
|
||||||
if wget -q --timeout=20 --tries=2 "$RELEASE_URL" -O "$TMP" && [ -s "$TMP" ]; then
|
|
||||||
# Sanity : an .xpi is a ZIP — must start with PK\x03\x04.
|
|
||||||
if head -c 2 "$TMP" | grep -q "PK"; then
|
|
||||||
install -m 0644 "$TMP" "$DEST"
|
|
||||||
chown secubox-toolbox:secubox-toolbox "$DEST" 2>/dev/null || true
|
|
||||||
log "fetched .xpi -> ${DEST} ($(stat -c%s "$DEST" 2>/dev/null) bytes)"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
log "downloaded file is not an .xpi (no release asset yet?) — keeping existing"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "fetch failed (no release yet / network) — /wg/toolbox.xpi will redirect to the release"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "wget missing — cannot fetch .xpi"
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
|
|
@ -622,7 +622,6 @@ pre{background:#1a1a25;color:var(--phos-hot);padding:0.6rem 0.8rem;border-radius
|
||||||
|
|
||||||
<div class="tab-content" data-content=android>
|
<div class="tab-content" data-content=android>
|
||||||
<a href="/wg/toolbox.apk" class="btn btn-go">📱 Installer l'app ToolBoX (1-tap)</a>
|
<a href="/wg/toolbox.apk" class="btn btn-go">📱 Installer l'app ToolBoX (1-tap)</a>
|
||||||
<a href="/wg/toolbox.xpi" class="btn">🧩 Extension navigateur (cartographie)</a>
|
|
||||||
<div class=warn style="margin-top:0.5rem">
|
<div class=warn style="margin-top:0.5rem">
|
||||||
✨ <b>Le plus simple</b> : l'app fait tout (CA + tunnel + vérif) en 5 étapes.
|
✨ <b>Le plus simple</b> : l'app fait tout (CA + tunnel + vérif) en 5 étapes.
|
||||||
Active « sources inconnues » à l'installation. Sinon, méthode manuelle ci-dessous :
|
Active « sources inconnues » à l'installation. Sinon, méthode manuelle ci-dessous :
|
||||||
|
|
@ -1193,7 +1192,6 @@ _ONBOARD_BODY = {
|
||||||
"android": """
|
"android": """
|
||||||
<p><b>✨ Le plus simple — l'app ToolBoX fait tout :</b></p>
|
<p><b>✨ Le plus simple — l'app ToolBoX fait tout :</b></p>
|
||||||
<a class=btn href="/wg/toolbox.apk">📱 Installer l'app ToolBoX (.apk, 1-tap)</a>
|
<a class=btn href="/wg/toolbox.apk">📱 Installer l'app ToolBoX (.apk, 1-tap)</a>
|
||||||
<a class=btn href="/wg/toolbox.xpi">🧩 Extension navigateur (cartographie live)</a>
|
|
||||||
<p class=note>Active « sources inconnues » à l'installation. L'app installe le CA, importe le tunnel et vérifie le R3 en 5 étapes. Sinon, méthode manuelle :</p>
|
<p class=note>Active « sources inconnues » à l'installation. L'app installe le CA, importe le tunnel et vérifie le R3 en 5 étapes. Sinon, méthode manuelle :</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Installe l'app <a class=btn href="https://play.google.com/store/apps/details?id=com.wireguard.android" target=_blank rel=noopener>WireGuard</a> depuis le Play Store.</li>
|
<li>Installe l'app <a class=btn href="https://play.google.com/store/apps/details?id=com.wireguard.android" target=_blank rel=noopener>WireGuard</a> depuis le Play Store.</li>
|
||||||
|
|
@ -1367,37 +1365,6 @@ async def wg_toolbox_apk() -> Response:
|
||||||
return RedirectResponse(url=_ANDROID_APK_RELEASE, status_code=302)
|
return RedirectResponse(url=_ANDROID_APK_RELEASE, status_code=302)
|
||||||
|
|
||||||
|
|
||||||
# Browser extension (Firefox .xpi), same serve pattern as the APK (#532).
|
|
||||||
# Tag-pinned URL (not /latest/): the webext release is published with
|
|
||||||
# make_latest:false so it does not steal "latest" from the Android APK
|
|
||||||
# release. Bump the tag here when a new webext-v* release is cut.
|
|
||||||
_WEBEXT_XPI = Path("/var/lib/secubox/toolbox/webext/secubox-toolbox-webext.xpi")
|
|
||||||
_WEBEXT_XPI_RELEASE = (
|
|
||||||
"https://github.com/CyberMind-FR/secubox-deb/releases/download/"
|
|
||||||
"webext-v0.1.1/secubox-toolbox-webext.xpi"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/wg/toolbox.xpi")
|
|
||||||
async def wg_toolbox_xpi() -> Response:
|
|
||||||
"""Serve the browser ToolBoX extension .xpi (#532).
|
|
||||||
|
|
||||||
Local file first (install from the cabine, works offline) ; if it
|
|
||||||
hasn't been fetched yet, 302 to the latest public GitHub release
|
|
||||||
asset so the onboard button never dead-ends.
|
|
||||||
"""
|
|
||||||
if _WEBEXT_XPI.exists() and _WEBEXT_XPI.stat().st_size > 0:
|
|
||||||
return Response(
|
|
||||||
content=_WEBEXT_XPI.read_bytes(),
|
|
||||||
media_type="application/x-xpinstall",
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": "attachment; filename=secubox-toolbox-webext.xpi",
|
|
||||||
"Cache-Control": "public, max-age=300",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return RedirectResponse(url=_WEBEXT_XPI_RELEASE, status_code=302)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/wg/ca.mobileconfig")
|
@router.get("/wg/ca.mobileconfig")
|
||||||
async def wg_ca_mobileconfig() -> Response:
|
async def wg_ca_mobileconfig() -> Response:
|
||||||
"""iOS profile that installs the mitm-wg CA in trust store."""
|
"""iOS profile that installs the mitm-wg CA in trust store."""
|
||||||
|
|
@ -1885,17 +1852,11 @@ _MITM_MODULES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _pull_mitm_module_events(mac_hash: str, limit: int = 50) -> dict:
|
def _pull_mitm_module_events(mac_hash: str) -> dict:
|
||||||
"""Query each receiving module's GET /mitm-events for this client.
|
"""Query each receiving module's GET /mitm-events for this client.
|
||||||
|
|
||||||
Returns a dict {module: {count, sample_events, enriched_summary}} for the
|
Returns a dict {module: {count, sample_events}} for the report. Errors per
|
||||||
report. Errors per module are non-fatal — if a module is down, it just
|
module are non-fatal — if a module is down, it just shows count=0.
|
||||||
shows count=0.
|
|
||||||
|
|
||||||
Phase 2c (#490) : also build an enriched_summary per module aggregating
|
|
||||||
the enrich_hook output (top apps from dpi, top providers from cookies,
|
|
||||||
devices from avatar, JA4 fingerprints from threat-analyst, score band
|
|
||||||
from soc).
|
|
||||||
"""
|
"""
|
||||||
import socket as _sock
|
import socket as _sock
|
||||||
import urllib.parse as _up
|
import urllib.parse as _up
|
||||||
|
|
@ -1911,17 +1872,15 @@ def _pull_mitm_module_events(mac_hash: str, limit: int = 50) -> dict:
|
||||||
self.sock.connect(sock_path)
|
self.sock.connect(sock_path)
|
||||||
|
|
||||||
conn = UDSConnection("localhost", timeout=2)
|
conn = UDSConnection("localhost", timeout=2)
|
||||||
qs = _up.urlencode({"mac_hash": mac_hash, "limit": limit})
|
qs = _up.urlencode({"mac_hash": mac_hash, "limit": 20})
|
||||||
conn.request("GET", f"/mitm-events?{qs}")
|
conn.request("GET", f"/mitm-events?{qs}")
|
||||||
resp = conn.getresponse()
|
resp = conn.getresponse()
|
||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
import json as _json
|
import json as _json
|
||||||
data = _json.loads(resp.read().decode("utf-8", errors="ignore")[:200000])
|
data = _json.loads(resp.read().decode("utf-8", errors="ignore")[:50000])
|
||||||
events = data.get("events", [])
|
|
||||||
out[kind] = {
|
out[kind] = {
|
||||||
"count": data.get("count", 0),
|
"count": data.get("count", 0),
|
||||||
"sample": events[:5],
|
"sample": data.get("events", [])[:5],
|
||||||
"enriched_summary": _summarize_enriched(kind, events),
|
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
out[kind] = {"count": 0, "error": f"HTTP {resp.status}"}
|
out[kind] = {"count": 0, "error": f"HTTP {resp.status}"}
|
||||||
|
|
@ -1933,88 +1892,6 @@ def _pull_mitm_module_events(mac_hash: str, limit: int = 50) -> dict:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _summarize_enriched(kind: str, events: list[dict]) -> dict:
|
|
||||||
"""Phase 2c (#490) : per-module aggregation of enrich_hook output.
|
|
||||||
|
|
||||||
Each receiving module attaches its enrich_hook result under 'enriched'
|
|
||||||
inside the event payload. This function consolidates them into a
|
|
||||||
compact summary suitable for the /report display.
|
|
||||||
"""
|
|
||||||
if not events:
|
|
||||||
return {}
|
|
||||||
if kind == "dpi":
|
|
||||||
apps: dict[str, dict] = {}
|
|
||||||
for ev in events:
|
|
||||||
e = (ev.get("payload") or {}).get("enriched") or {}
|
|
||||||
app = e.get("app")
|
|
||||||
if not app or app == "?":
|
|
||||||
continue
|
|
||||||
if app not in apps:
|
|
||||||
apps[app] = {"count": 0, "category": e.get("category"), "emoji": e.get("emoji")}
|
|
||||||
apps[app]["count"] += 1
|
|
||||||
top = sorted([{"app": k, **v} for k, v in apps.items()], key=lambda x: -x["count"])[:15]
|
|
||||||
return {"top_apps": top, "classified_events": sum(v["count"] for v in apps.values())}
|
|
||||||
if kind == "cookies":
|
|
||||||
providers: dict[str, dict] = {}
|
|
||||||
total_trackers = 0
|
|
||||||
for ev in events:
|
|
||||||
e = (ev.get("payload") or {}).get("enriched") or {}
|
|
||||||
for p, info in (e.get("providers") or {}).items():
|
|
||||||
if p not in providers:
|
|
||||||
providers[p] = {"count": 0, "category": info.get("category"), "emoji": info.get("emoji")}
|
|
||||||
providers[p]["count"] += info.get("count", 1)
|
|
||||||
total_trackers += info.get("count", 1)
|
|
||||||
top = sorted([{"provider": k, **v} for k, v in providers.items()], key=lambda x: -x["count"])[:10]
|
|
||||||
return {"top_providers": top, "tracker_total": total_trackers}
|
|
||||||
if kind == "avatar":
|
|
||||||
devices: dict[str, dict] = {}
|
|
||||||
browsers: dict[str, dict] = {}
|
|
||||||
for ev in events:
|
|
||||||
e = (ev.get("payload") or {}).get("enriched") or {}
|
|
||||||
d = e.get("device")
|
|
||||||
if d and d != "unknown":
|
|
||||||
if d not in devices:
|
|
||||||
devices[d] = {"count": 0, "emoji": e.get("device_emoji"), "os_label": e.get("os_label")}
|
|
||||||
devices[d]["count"] += 1
|
|
||||||
b = e.get("browser")
|
|
||||||
if b and b != "unknown":
|
|
||||||
if b not in browsers:
|
|
||||||
browsers[b] = {"count": 0, "emoji": e.get("browser_emoji"), "label": e.get("browser_label")}
|
|
||||||
browsers[b]["count"] += 1
|
|
||||||
return {"devices": devices, "browsers": browsers}
|
|
||||||
if kind == "threat-analyst":
|
|
||||||
fps: dict[str, dict] = {}
|
|
||||||
for ev in events:
|
|
||||||
e = (ev.get("payload") or {}).get("enriched") or {}
|
|
||||||
fp = e.get("ja4_fingerprint")
|
|
||||||
if not fp:
|
|
||||||
continue
|
|
||||||
if fp not in fps:
|
|
||||||
fps[fp] = {
|
|
||||||
"count": 0,
|
|
||||||
"known_client": e.get("known_client"),
|
|
||||||
"raw_repr": e.get("ja4_raw_repr"),
|
|
||||||
}
|
|
||||||
fps[fp]["count"] += 1
|
|
||||||
top = sorted([{"fingerprint": k, **v} for k, v in fps.items()], key=lambda x: -x["count"])[:10]
|
|
||||||
return {"top_fingerprints": top, "unique_count": len(fps)}
|
|
||||||
if kind == "soc":
|
|
||||||
total_w = 0
|
|
||||||
kinds_seen: dict[str, int] = {}
|
|
||||||
max_band = "low"
|
|
||||||
band_order = ["low", "medium", "high"]
|
|
||||||
for ev in events:
|
|
||||||
e = (ev.get("payload") or {}).get("enriched") or {}
|
|
||||||
total_w += e.get("total_weight") or 0
|
|
||||||
for k in e.get("indicator_kinds") or []:
|
|
||||||
kinds_seen[k] = kinds_seen.get(k, 0) + 1
|
|
||||||
b = e.get("band") or "low"
|
|
||||||
if band_order.index(b) > band_order.index(max_band):
|
|
||||||
max_band = b
|
|
||||||
return {"total_weight": total_w, "max_band": max_band, "indicator_kinds": kinds_seen}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def _enrich_with_geo(matches: list[dict]) -> list[dict]:
|
def _enrich_with_geo(matches: list[dict]) -> list[dict]:
|
||||||
"""Add geo info to threat_intel matches."""
|
"""Add geo info to threat_intel matches."""
|
||||||
out = []
|
out = []
|
||||||
|
|
|
||||||
|
|
@ -682,39 +682,12 @@ def fold_recent(window_seconds: int = 300) -> Tuple[int, int]:
|
||||||
return nodes_touched, links_touched
|
return nodes_touched, links_touched
|
||||||
|
|
||||||
|
|
||||||
# eTLD+1 rollup (#549). Mirror of the addon's _registrable_domain so the
|
|
||||||
# graph can group trackers under their registrable parent (all
|
|
||||||
# *.doubleclick.net → doubleclick.net) without a publicsuffix dependency.
|
|
||||||
_MULTI_LABEL_TLDS = {
|
|
||||||
"co.uk", "ac.uk", "gov.uk", "org.uk", "net.uk",
|
|
||||||
"co.jp", "ne.jp", "ac.jp",
|
|
||||||
"com.au", "net.au", "org.au",
|
|
||||||
"com.br", "com.cn", "com.hk", "com.tw", "com.mx",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _registrable_domain(host: str) -> str:
|
|
||||||
"""Cheap eTLD+1 : www.lemonde.fr → lemonde.fr ; a.b.example.co.uk →
|
|
||||||
example.co.uk. Raw IPs and single-label hosts pass through."""
|
|
||||||
h = (host or "").lower().strip(".")
|
|
||||||
if not h or h.replace(".", "").replace(":", "").isdigit():
|
|
||||||
return h
|
|
||||||
parts = h.split(".")
|
|
||||||
if len(parts) < 2:
|
|
||||||
return h
|
|
||||||
last_two = ".".join(parts[-2:])
|
|
||||||
if last_two in _MULTI_LABEL_TLDS and len(parts) >= 3:
|
|
||||||
return ".".join(parts[-3:])
|
|
||||||
return last_two
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||||
"""Return the per-client graph JSON contract.
|
"""Return the per-client graph JSON contract.
|
||||||
|
|
||||||
{nodes:[{id,domain,family,hits,sites_count}],
|
{nodes:[{id,domain,family,hits,sites_count}],
|
||||||
edges:[{src,dst,reuse_count,shared_trackers[],ja4_match}],
|
edges:[{src,dst,reuse_count,shared_trackers[],ja4_match}],
|
||||||
stats:{total_trackers,total_sites,first_seen,last_seen},
|
stats:{total_trackers,total_sites,first_seen,last_seen}}
|
||||||
by_domain:[...], targets:[...], history:[...]} # additive (#549)
|
|
||||||
"""
|
"""
|
||||||
since = int(time.time()) - max(since_seconds, 3600)
|
since = int(time.time()) - max(since_seconds, 3600)
|
||||||
out: Dict = {"nodes": [], "edges": [], "stats": {}}
|
out: Dict = {"nodes": [], "edges": [], "stats": {}}
|
||||||
|
|
@ -797,84 +770,9 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||||
# Phase 12.C — operator-grade / state-adjacent surfaces.
|
# Phase 12.C — operator-grade / state-adjacent surfaces.
|
||||||
opgrade = opgrade_for_client(mac_hash, since_seconds=since_seconds)
|
opgrade = opgrade_for_client(mac_hash, since_seconds=since_seconds)
|
||||||
out["opgrade"] = opgrade
|
out["opgrade"] = opgrade
|
||||||
# ── #549 additive aggregations (read-time, no schema change) ──
|
|
||||||
# (a) by_domain : roll trackers up under registrable parent.
|
|
||||||
_dom: Dict[str, dict] = {}
|
|
||||||
for n in out["nodes"]:
|
|
||||||
parent = _registrable_domain(n["domain"])
|
|
||||||
d = _dom.setdefault(parent, {
|
|
||||||
"domain": parent, "tracker_count": 0, "hits": 0,
|
|
||||||
"_trackers": set(), "_sites": set(), "_vendors": set(),
|
|
||||||
"last_seen": 0,
|
|
||||||
})
|
|
||||||
d["_trackers"].add(n["domain"])
|
|
||||||
d["hits"] += n["hits"] or 0
|
|
||||||
d["_sites"].update(n["sites"])
|
|
||||||
d["last_seen"] = max(d["last_seen"], n["last_seen"] or 0)
|
|
||||||
for v in (n.get("cdn_vendor"), n.get("antibot_vendor"),
|
|
||||||
n.get("opgrade_vendor")):
|
|
||||||
if v:
|
|
||||||
d["_vendors"].add(v)
|
|
||||||
by_domain = []
|
|
||||||
for d in _dom.values():
|
|
||||||
by_domain.append({
|
|
||||||
"domain": d["domain"],
|
|
||||||
"tracker_count": len(d["_trackers"]),
|
|
||||||
"trackers": sorted(d["_trackers"])[:30],
|
|
||||||
"hits": d["hits"],
|
|
||||||
"sites_count": len(d["_sites"]),
|
|
||||||
"sites": sorted(d["_sites"])[:20],
|
|
||||||
"vendors": sorted(d["_vendors"]),
|
|
||||||
"last_seen": d["last_seen"],
|
|
||||||
})
|
|
||||||
by_domain.sort(key=lambda x: (-x["hits"], -x["tracker_count"]))
|
|
||||||
out["by_domain"] = by_domain
|
|
||||||
|
|
||||||
# (b) targets : invert sites→trackers (who watches each page).
|
|
||||||
_tgt: Dict[str, dict] = {}
|
|
||||||
for n in out["nodes"]:
|
|
||||||
for s in n["sites"]:
|
|
||||||
t = _tgt.setdefault(s, {
|
|
||||||
"site": s, "hits": 0,
|
|
||||||
"_trackers": set(), "_domains": set(),
|
|
||||||
})
|
|
||||||
t["_trackers"].add(n["domain"])
|
|
||||||
t["_domains"].add(_registrable_domain(n["domain"]))
|
|
||||||
t["hits"] += n["hits"] or 0
|
|
||||||
targets = []
|
|
||||||
for t in _tgt.values():
|
|
||||||
targets.append({
|
|
||||||
"site": t["site"],
|
|
||||||
"tracker_count": len(t["_trackers"]),
|
|
||||||
"trackers": sorted(t["_trackers"])[:30],
|
|
||||||
"parent_domains": sorted(t["_domains"]),
|
|
||||||
"hits": t["hits"],
|
|
||||||
})
|
|
||||||
targets.sort(key=lambda x: (-x["tracker_count"], -x["hits"]))
|
|
||||||
out["targets"] = targets
|
|
||||||
|
|
||||||
# (c) history : per-(UTC)day timeline from the raw edge log.
|
|
||||||
history = []
|
|
||||||
for r in c.execute(
|
|
||||||
"SELECT (ts/86400) AS day_epoch, COUNT(*) AS hits, "
|
|
||||||
"COUNT(DISTINCT tracker_domain) AS trackers, "
|
|
||||||
"COUNT(DISTINCT src_site) AS sites "
|
|
||||||
"FROM social_edges WHERE client_mac_hash = ? AND ts >= ? "
|
|
||||||
"GROUP BY day_epoch ORDER BY day_epoch",
|
|
||||||
(mac_hash, since),
|
|
||||||
).fetchall():
|
|
||||||
history.append({
|
|
||||||
"day": int(r["day_epoch"]) * 86400,
|
|
||||||
"hits": r["hits"],
|
|
||||||
"trackers": r["trackers"],
|
|
||||||
"sites": r["sites"],
|
|
||||||
})
|
|
||||||
out["history"] = history
|
|
||||||
|
|
||||||
out["stats"] = {
|
out["stats"] = {
|
||||||
"total_trackers": (stats_row["total_trackers"] or 0) if stats_row else 0,
|
"total_trackers": (stats_row["total_trackers"] or 0) if stats_row else 0,
|
||||||
"total_sites": sites_count,
|
"total_sites": sites_count,
|
||||||
"total_domains": len(by_domain),
|
|
||||||
"first_seen": stats_row["first_seen"] if stats_row else None,
|
"first_seen": stats_row["first_seen"] if stats_row else None,
|
||||||
"last_seen": stats_row["last_seen"] if stats_row else None,
|
"last_seen": stats_row["last_seen"] if stats_row else None,
|
||||||
"antibot_sites": len({a["src_site"] for a in antibot}),
|
"antibot_sites": len({a["src_site"] for a in antibot}),
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
|
||||||
# 📱 Android ToolBox — one-tap R3 onboarding
|
|
||||||
|
|
||||||
The **SecuBox Android ToolBox** is a tiny companion app that onboards a
|
|
||||||
phone onto the VILLAGE3B *cabine* in one tap: it installs the cabine CA,
|
|
||||||
brings up the WireGuard R3 tunnel, verifies reachability, then opens the
|
|
||||||
live *cartographie sociale*. It replaces the manual Android tutorial.
|
|
||||||
|
|
||||||
- Source : [`clients/android-toolbox/`](https://github.com/CyberMind-FR/secubox-deb/tree/master/clients/android-toolbox)
|
|
||||||
- Package : `in.secubox.toolbox` · Kotlin + Jetpack Compose · minSdk 26 / targetSdk 34
|
|
||||||
- License : `LicenseRef-CMSD-1.0`
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Grab the APK directly from the cabine — the toolbox serves it:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://kbin.<board>.secubox.in/wg/toolbox.apk
|
|
||||||
```
|
|
||||||
|
|
||||||
The onboard panels in the kbin WebUI expose a **📱 Installer l'app ToolBoX
|
|
||||||
(1-tap)** button pointing at that endpoint. When the cabine has a locally
|
|
||||||
fetched build it serves it (`application/vnd.android.package-archive`);
|
|
||||||
otherwise it 302-redirects to the latest GitHub release asset
|
|
||||||
`secubox-toolbox-android.apk`.
|
|
||||||
|
|
||||||
> The MVP APK is **debug-signed** (sideload — enable *Install unknown
|
|
||||||
> apps*). A release-signed build with a published fingerprint is a
|
|
||||||
> follow-up (needs a keystore secret in CI).
|
|
||||||
|
|
||||||
## Onboarding flows
|
|
||||||
|
|
||||||
### Manual path (non-rooted)
|
|
||||||
1. **Discover** — scan the kbin QR or type the booth host (`kbin.<board>.secubox.in`).
|
|
||||||
2. **Install CA** — downloads `/wg/ca.crt`, launches the Android cert-install
|
|
||||||
intent (`KeyChain.createInstallIntent`).
|
|
||||||
3. **Import profile** — downloads `/wg/profile/new`, hands the `.conf` to the
|
|
||||||
official WireGuard app (`FileProvider` + `ACTION_VIEW`).
|
|
||||||
4. **Verify** — polls `/wg/r3-check` → *Tunnel R3 actif ✓*.
|
|
||||||
5. **Live metrics** — opens `/social/me` (cartographie sociale).
|
|
||||||
|
|
||||||
Android 11+ restricts **user CA trust**, so on a non-rooted device the
|
|
||||||
browser CA confirm is a guided manual step.
|
|
||||||
|
|
||||||
### Root path — fully-automated, silent (#538)
|
|
||||||
When the device is **rooted**, the Discover step shows an extra
|
|
||||||
**⚡ Installation automatique (root)** button. One tap runs everything with
|
|
||||||
no further interaction (a `RootAuto` step streams the progress log):
|
|
||||||
|
|
||||||
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
|
|
||||||
`subject_hash_old` in pure Kotlin, then bind-mounts a populated copy of
|
|
||||||
the trust store over `/system/etc/security/cacerts` (+ the conscrypt
|
|
||||||
APEX on Android 14), restoring the SELinux context
|
|
||||||
`u:object_r:system_security_cacerts_file:s0`. **Every** app trusts the
|
|
||||||
cabine CA — not just user-CA opt-in apps. Reversible via `umount`.
|
|
||||||
2. **Native WireGuard** — if the kernel has the WireGuard module + `wg`/`ip`,
|
|
||||||
the tunnel comes up natively (`ip link add … type wireguard` + `wg set`) —
|
|
||||||
no WireGuard app required.
|
|
||||||
3. **Auto R3 verify** — polls `/wg/r3-check`.
|
|
||||||
|
|
||||||
If the kernel lacks WireGuard, the root path installs the system CA then
|
|
||||||
falls back to the manual WireGuard-app handoff.
|
|
||||||
|
|
||||||
**Safety** — every root action is gated behind the explicit tap; nothing
|
|
||||||
runs as root unless the operator chooses root mode on their own device.
|
|
||||||
Code: `RootShell.kt` (su wrapper) + `RootOnboard.kt` (silent sequence).
|
|
||||||
|
|
||||||
## Build (CI)
|
|
||||||
|
|
||||||
No Gradle wrapper jar is committed (text-only scaffold). CI builds it:
|
|
||||||
|
|
||||||
- GitHub Actions `build-android-apk.yml` → debug APK artifact on push to
|
|
||||||
`master` / PRs touching `clients/android-toolbox/**`.
|
|
||||||
- Tagging `android-v*` publishes the APK as a release asset.
|
|
||||||
|
|
||||||
Locally (Android SDK + Gradle 8.9 + JDK 17):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd clients/android-toolbox
|
|
||||||
gradle :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cabine endpoints consumed
|
|
||||||
|
|
||||||
| Endpoint | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `/wg/ca.crt` / `/wg/ca.pem` | cabine CA (user / system store) |
|
|
||||||
| `/wg/profile/new` | fresh WireGuard `.conf` |
|
|
||||||
| `/wg/r3-check` | tunnel reachability probe |
|
|
||||||
| `/social/me` | live cartographie sociale |
|
|
||||||
| `/wg/toolbox.apk` | the APK itself |
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
|
||||||
# 🧩 Browser extension — Cartographie sociale live
|
|
||||||
|
|
||||||
The **SecuBox ToolBoX browser extension** *emancipates* the R3 toolbox live
|
|
||||||
tracker analysis into your browser. Instead of only seeing the *cartographie
|
|
||||||
sociale* on `kbin/social/me`, a toolbar badge ticks up as trackers fire and a
|
|
||||||
popup shows who is watching you — live, as you browse.
|
|
||||||
|
|
||||||
Sibling of the [[Android-ToolBox]] app. Talks **only** to your cabine over the
|
|
||||||
R3 tunnel — no third-party calls.
|
|
||||||
|
|
||||||
- Source : [`clients/webext-toolbox/`](https://github.com/CyberMind-FR/secubox-deb/tree/master/clients/webext-toolbox)
|
|
||||||
- WebExtension **MV3** (Firefox `.xpi` + Chromium) · plain JS/HTML/CSS, no bundler
|
|
||||||
- License : `LicenseRef-CMSD-1.0`
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Published release `.xpi` (downloadable directly):
|
|
||||||
|
|
||||||
```
|
|
||||||
https://github.com/CyberMind-FR/secubox-deb/releases/download/webext-v0.1.1/secubox-toolbox-webext.xpi
|
|
||||||
```
|
|
||||||
|
|
||||||
The toolbox also serves it from the cabine:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://kbin.<board>.secubox.in/wg/toolbox.xpi
|
|
||||||
```
|
|
||||||
|
|
||||||
The kbin onboard panel exposes a **🧩 Extension navigateur (cartographie)**
|
|
||||||
button. When a local build is present the cabine serves it
|
|
||||||
(`application/x-xpinstall`); otherwise it 302-redirects to the **tag-pinned**
|
|
||||||
release asset above. The webext release is published `make_latest:false` so it
|
|
||||||
does not steal the repo "Latest" pointer from the Android APK release.
|
|
||||||
|
|
||||||
- **Firefox** — open the `.xpi`. A permanent install needs an AMO-signed build
|
|
||||||
(release CI / `web-ext sign`); for development use *about:debugging → Load
|
|
||||||
Temporary Add-on*, or an ESR/Dev build with
|
|
||||||
`xpinstall.signatures.required=false`.
|
|
||||||
- **Chromium** — load unpacked (`chrome://extensions` → Developer mode).
|
|
||||||
Chromium action icons must be raster — rasterise `icons/icon.svg` to PNG
|
|
||||||
before a Web Store build (Firefox accepts the SVG as-is).
|
|
||||||
|
|
||||||
## What it does
|
|
||||||
|
|
||||||
- **Pairing** — calls `/social/me` over the tunnel, which 303-redirects to
|
|
||||||
`/social/{token}`; the extension reads the minted HMAC token from the final
|
|
||||||
URL. Anonymous (rotating `mac_hash`), no account. Manual token entry in the
|
|
||||||
options page.
|
|
||||||
- **Live badge** — the toolbar icon shows the live tracker count (polled once a
|
|
||||||
minute). Colour escalates: 🟡 gold → 🟥 anti-bot present → 🟪 operator-grade
|
|
||||||
present.
|
|
||||||
- **Popup** — four stat tiles (trackers / sites / anti-bot / operator-grade), a
|
|
||||||
dependency-free **mini Round-Eye graph** (device centre, trackers on the ring,
|
|
||||||
radius by hits, colour by tier), and a top-tracker list tagged with CDN
|
|
||||||
(12.A) / anti-bot (12.B) / operator-grade (12.C).
|
|
||||||
- **Actions** — *Cartographie complète* (full d3 view at `/social/{token}`),
|
|
||||||
*Rapport PDF* (`/social/report/{token}.pdf`), *Effacer mes données* (RGPD
|
|
||||||
art. 17 wipe → `POST /social/wipe/{token}`).
|
|
||||||
|
|
||||||
## Build (CI)
|
|
||||||
|
|
||||||
No bundler — `build-webext.yml` runs `web-ext lint` then packages the `.xpi`:
|
|
||||||
|
|
||||||
- artifact on push to `master` / PRs touching `clients/webext-toolbox/**`
|
|
||||||
- tagging `webext-v*` publishes the `.xpi` as a release asset
|
|
||||||
|
|
||||||
Locally:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd clients/webext-toolbox
|
|
||||||
./build.sh # → secubox-toolbox-webext-<version>.xpi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cabine endpoints consumed
|
|
||||||
|
|
||||||
| Endpoint | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `/wg/r3-check` | tunnel presence indicator |
|
|
||||||
| `/social/me` | pair → mint token (303 → `/social/{token}`) |
|
|
||||||
| `/social/graph/{token}?since=` | per-session tracker graph JSON |
|
|
||||||
| `/social/wipe/{token}` | RGPD art. 17 erasure |
|
|
||||||
| `/social/{token}` | full d3 cartographie page |
|
|
||||||
| `/social/report/{token}.pdf` | bilingual PDF report |
|
|
||||||
| `/wg/toolbox.xpi` | the extension itself |
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- No server-side CORS needed: an MV3 extension with `host_permissions` for
|
|
||||||
`*.secubox.in` fetches cross-origin from its background without CORS.
|
|
||||||
- MVP polls `/social/graph` and computes the delta client-side; a future
|
|
||||||
`GET /social/live/{token}` (SSE) can replace the poll. The deception-plane
|
|
||||||
*Poke/Emancipate* per-site control lands once the deception plane ships.
|
|
||||||
|
|
@ -29,8 +29,6 @@
|
||||||
* [[ARM-Installation]] | [FR](ARM-Installation-FR) | [DE](ARM-Installation-DE) | [中文](ARM-Installation-ZH)
|
* [[ARM-Installation]] | [FR](ARM-Installation-FR) | [DE](ARM-Installation-DE) | [中文](ARM-Installation-ZH)
|
||||||
* [[ESPRESSObin]] | [FR](ESPRESSObin-FR) | [DE](ESPRESSObin-DE) | [中文](ESPRESSObin-ZH)
|
* [[ESPRESSObin]] | [FR](ESPRESSObin-FR) | [DE](ESPRESSObin-DE) | [中文](ESPRESSObin-ZH)
|
||||||
* [[Eye-Remote]] 📡
|
* [[Eye-Remote]] 📡
|
||||||
* [[Android-ToolBox]] 📱 one-tap R3
|
|
||||||
* [[Browser-Extension]] 🧩 cartographie
|
|
||||||
* [[QEMU-ARM64]] 🖥️
|
* [[QEMU-ARM64]] 🖥️
|
||||||
|
|
||||||
### 🟢 ROOT — Configuration
|
### 🟢 ROOT — Configuration
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user