mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 10:47:30 +00:00
Compare commits
2 Commits
1b64d2c546
...
75e414eb01
| Author | SHA1 | Date | |
|---|---|---|---|
| 75e414eb01 | |||
| 4d4dacccdf |
|
|
@ -74,6 +74,9 @@ a:hover{text-decoration:underline}
|
||||||
<a href="/report/me/html" class=qi title="Mon rapport live">
|
<a href="/report/me/html" class=qi title="Mon rapport live">
|
||||||
<span class=qi-emoji>📊</span><span class=qi-label>Mon rapport</span>
|
<span class=qi-emoji>📊</span><span class=qi-label>Mon rapport</span>
|
||||||
</a>
|
</a>
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
<a href="/wg/ca.mobileconfig" class=qi title="CA R3 iPhone (.mobileconfig)">
|
<a href="/wg/ca.mobileconfig" class=qi title="CA R3 iPhone (.mobileconfig)">
|
||||||
<span class=qi-emoji>📲</span><span class=qi-label>CA iPhone</span>
|
<span class=qi-emoji>📲</span><span class=qi-label>CA iPhone</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,17 @@ case "$1" in
|
||||||
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox
|
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/lib/secubox/toolbox
|
||||||
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/log/secubox
|
install -d -m 0750 -o secubox-toolbox -g secubox-toolbox /var/log/secubox
|
||||||
|
|
||||||
|
# 4a. Phase 11.B (#507) — make /usr/share/secubox/www traversable so
|
||||||
|
# the FastAPI StaticFiles mount can serve /toolbox/social.{css,js} +
|
||||||
|
# /toolbox/d3.v7.min.js to clients arriving via the kbin HAProxy
|
||||||
|
# route (which bypasses nginx and so doesn't use the nginx /toolbox/
|
||||||
|
# alias). Without this chmod the uvicorn process crashes at
|
||||||
|
# startup with PermissionError on Path("/usr/share/secubox/www/
|
||||||
|
# toolbox").is_dir(). Idempotent ; safe to repeat.
|
||||||
|
if [ -d /usr/share/secubox/www ]; then
|
||||||
|
chmod 0755 /usr/share/secubox/www 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# 4b. GeoLite2 databases (Phase 2a+ : flag emojis + ASN org)
|
# 4b. GeoLite2 databases (Phase 2a+ : flag emojis + ASN org)
|
||||||
# ASN DB from geoipupdate or Debian package geoip-database
|
# ASN DB from geoipupdate or Debian package geoip-database
|
||||||
# Country DB from db-ip.com CC-BY (no MaxMind account required)
|
# Country DB from db-ip.com CC-BY (no MaxMind account required)
|
||||||
|
|
|
||||||
|
|
@ -82,15 +82,50 @@ def _registrable_domain(host: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
# ─── peer identity ───
|
# ─── peer identity ───
|
||||||
# We reuse the local_store helpers so the social addon sees the SAME
|
# Phase 11.A originally tried `from . import local_store` which silently
|
||||||
# mac_hash the rest of the toolbox does. Imported lazily to avoid a
|
# failed because mitmproxy loads addons as top-level modules (not as
|
||||||
# circular import at addon-load time.
|
# package members), so the relative import never resolved. Inlined
|
||||||
|
# here — only the R3 path (peer IP in 10.99.1.0/24 → WG pubkey hash)
|
||||||
|
# since Phase B is R3-only. R2 captive lookup remains in local_store
|
||||||
|
# and joins later when the addon is wired into the captive mitm.
|
||||||
|
import hashlib as _hashlib
|
||||||
|
import json as _json
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
|
_WG_PEERS_DB = _Path("/var/lib/secubox/toolbox/wg-peers.json")
|
||||||
|
_WG_PEERS_CACHE: dict = {}
|
||||||
|
_WG_PEERS_MTIME: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _wg_hash_of(ip: str) -> Optional[str]:
|
||||||
|
global _WG_PEERS_MTIME
|
||||||
|
try:
|
||||||
|
if not _WG_PEERS_DB.exists():
|
||||||
|
return None
|
||||||
|
mtime = _WG_PEERS_DB.stat().st_mtime
|
||||||
|
if mtime != _WG_PEERS_MTIME or not _WG_PEERS_CACHE:
|
||||||
|
data = _json.loads(_WG_PEERS_DB.read_text()).get("peers", {})
|
||||||
|
_WG_PEERS_CACHE.clear()
|
||||||
|
for pubkey, meta in data.items():
|
||||||
|
peer_ip = meta.get("ip")
|
||||||
|
if peer_ip:
|
||||||
|
_WG_PEERS_CACHE[peer_ip] = _hashlib.sha256(
|
||||||
|
pubkey.encode()
|
||||||
|
).hexdigest()[:16]
|
||||||
|
_WG_PEERS_MTIME = mtime
|
||||||
|
return _WG_PEERS_CACHE.get(ip)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _client_mac_hash(flow) -> Optional[str]:
|
def _client_mac_hash(flow) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
from . import local_store as _ls # type: ignore
|
if flow.client_conn and flow.client_conn.peername:
|
||||||
ip = _ls._peer_ip(flow)
|
ip = flow.client_conn.peername[0]
|
||||||
return _ls._client_hash(ip)
|
if ip and ip.startswith("10.99.1."):
|
||||||
|
return _wg_hash_of(ip)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2103,6 +2103,71 @@ def _load_social_i18n(lang: str) -> dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/social/me")
|
||||||
|
async def social_view_me(request: Request) -> RedirectResponse:
|
||||||
|
"""Self-resolving entry point used by the kbin splash menu.
|
||||||
|
|
||||||
|
Resolution chain (matches /report/me/html):
|
||||||
|
1. ?mh=<hash> explicit R3 hash in URL (e.g. from banner)
|
||||||
|
2. X-R3-Peer header mitm-wg's inject_xff sentinel for transparent
|
||||||
|
R3 flows (peer IP in 10.99.1.0/24)
|
||||||
|
3. _resolve() ARP lookup R2 captive subnet clients only
|
||||||
|
|
||||||
|
Mints a short-TTL (1 h) HMAC token bound to the resolved mac_hash and
|
||||||
|
redirects to /social/{token}. Keeps a single HMAC-token-gated code
|
||||||
|
path for the graph view + wipe endpoint.
|
||||||
|
"""
|
||||||
|
salt = _get_salt()
|
||||||
|
mac_hash = None
|
||||||
|
|
||||||
|
# 1) ?mh= overrides everything (used by inject_banner.py links + the
|
||||||
|
# report flow + manual curl).
|
||||||
|
mh_qp = (request.query_params.get("mh") or "").strip().lower()
|
||||||
|
if mh_qp and all(c in "0123456789abcdef" for c in mh_qp) and 8 <= len(mh_qp) <= 64:
|
||||||
|
mac_hash = mh_qp
|
||||||
|
|
||||||
|
# 2) X-R3-Peer (transparent R3 — the iPhone hits kbin via the R3
|
||||||
|
# tunnel, mitm-wg adds the sentinel, HAProxy passes it through).
|
||||||
|
# We derive the same mac_hash the local_store addon uses :
|
||||||
|
# sha256(wg_pubkey)[:16] looked up from /var/lib/secubox/toolbox/
|
||||||
|
# wg-peers.json keyed by peer IP.
|
||||||
|
if not mac_hash:
|
||||||
|
peer_ip = _client_ip(request)
|
||||||
|
if peer_ip and peer_ip.startswith("10.99.1."):
|
||||||
|
try:
|
||||||
|
import hashlib as _h
|
||||||
|
import json as _j
|
||||||
|
from pathlib import Path as _P
|
||||||
|
_DB = _P("/var/lib/secubox/toolbox/wg-peers.json")
|
||||||
|
if _DB.exists():
|
||||||
|
peers = _j.loads(_DB.read_text()).get("peers", {})
|
||||||
|
for pubkey, meta in peers.items():
|
||||||
|
if meta.get("ip") == peer_ip:
|
||||||
|
mac_hash = _h.sha256(pubkey.encode()).hexdigest()[:16]
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("/social/me wg-peer lookup failed: %s", e)
|
||||||
|
|
||||||
|
# 3) R2 captive — ARP _resolve().
|
||||||
|
if not mac_hash:
|
||||||
|
_ip, mac = _resolve(request)
|
||||||
|
if mac:
|
||||||
|
mac_hash = macmod.hash_mac(mac, salt)
|
||||||
|
|
||||||
|
if not mac_hash:
|
||||||
|
raise HTTPException(
|
||||||
|
400,
|
||||||
|
"client identity unresolved (not on R3 tunnel and not in "
|
||||||
|
"captive subnet) — append ?mh=<hash> from your banner's "
|
||||||
|
"report link",
|
||||||
|
)
|
||||||
|
|
||||||
|
tok = reports.mint_token(mac_hash, salt, ttl_seconds=3600)
|
||||||
|
lang = request.query_params.get("lang") or ""
|
||||||
|
suffix = f"?lang={lang}" if lang else ""
|
||||||
|
return RedirectResponse(url=f"/social/{tok.token}{suffix}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/social/{token}", response_class=HTMLResponse)
|
@router.get("/social/{token}", response_class=HTMLResponse)
|
||||||
async def social_view(token: str, request: Request) -> HTMLResponse:
|
async def social_view(token: str, request: Request) -> HTMLResponse:
|
||||||
"""Per-client social mapping HTML page.
|
"""Per-client social mapping HTML page.
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from . import __version__, social, store, threat_intel
|
from . import __version__, social, store, threat_intel
|
||||||
from .api import router as toolbox_router
|
from .api import router as toolbox_router
|
||||||
|
|
@ -26,6 +28,14 @@ app = FastAPI(
|
||||||
)
|
)
|
||||||
app.include_router(toolbox_router)
|
app.include_router(toolbox_router)
|
||||||
|
|
||||||
|
# Phase 11.B (#507) — serve the WebUI assets on the same origin as
|
||||||
|
# the FastAPI HTML pages. Required because the kbin vhost routes
|
||||||
|
# through HAProxy directly to uvicorn (bypassing nginx), so the
|
||||||
|
# nginx /toolbox/ alias never gets a chance to match.
|
||||||
|
_TOOLBOX_WWW = Path("/usr/share/secubox/www/toolbox")
|
||||||
|
if _TOOLBOX_WWW.is_dir():
|
||||||
|
app.mount("/toolbox", StaticFiles(directory=_TOOLBOX_WWW), name="toolbox-www")
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def _startup() -> None:
|
async def _startup() -> None:
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@
|
||||||
}
|
}
|
||||||
async function confirmWipe() {
|
async function confirmWipe() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/v1/toolbox/social/wipe/${encodeURIComponent(token)}`, { method: 'POST' });
|
const r = await fetch(`/social/wipe/${encodeURIComponent(token)}`, { method: 'POST' });
|
||||||
if (!r.ok) throw new Error('http ' + r.status);
|
if (!r.ok) throw new Error('http ' + r.status);
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
wipeModal.close();
|
wipeModal.close();
|
||||||
|
|
@ -226,7 +226,7 @@
|
||||||
// ─── fetch + bootstrap ───
|
// ─── fetch + bootstrap ───
|
||||||
async function fetchGraph() {
|
async function fetchGraph() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/v1/toolbox/social/graph/${encodeURIComponent(token)}?since=86400`);
|
const r = await fetch(`/social/graph/${encodeURIComponent(token)}?since=86400`);
|
||||||
if (!r.ok) throw new Error('http ' + r.status);
|
if (!r.ok) throw new Error('http ' + r.status);
|
||||||
const g = await r.json();
|
const g = await r.json();
|
||||||
render(g);
|
render(g);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user