mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 08:26:08 +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">
|
||||
<span class=qi-emoji>📊</span><span class=qi-label>Mon rapport</span>
|
||||
</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)">
|
||||
<span class=qi-emoji>📲</span><span class=qi-label>CA iPhone</span>
|
||||
</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/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)
|
||||
# ASN DB from geoipupdate or Debian package geoip-database
|
||||
# Country DB from db-ip.com CC-BY (no MaxMind account required)
|
||||
|
|
|
|||
|
|
@ -82,18 +82,53 @@ def _registrable_domain(host: str) -> str:
|
|||
|
||||
|
||||
# ─── peer identity ───
|
||||
# We reuse the local_store helpers so the social addon sees the SAME
|
||||
# mac_hash the rest of the toolbox does. Imported lazily to avoid a
|
||||
# circular import at addon-load time.
|
||||
def _client_mac_hash(flow) -> Optional[str]:
|
||||
# Phase 11.A originally tried `from . import local_store` which silently
|
||||
# failed because mitmproxy loads addons as top-level modules (not as
|
||||
# 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:
|
||||
from . import local_store as _ls # type: ignore
|
||||
ip = _ls._peer_ip(flow)
|
||||
return _ls._client_hash(ip)
|
||||
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]:
|
||||
try:
|
||||
if flow.client_conn and flow.client_conn.peername:
|
||||
ip = flow.client_conn.peername[0]
|
||||
if ip and ip.startswith("10.99.1."):
|
||||
return _wg_hash_of(ip)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ─── cookie parsers ───
|
||||
_SET_COOKIE_NAMEVAL = re.compile(r"^\s*([^=;]+)\s*=\s*([^;]*)")
|
||||
_COOKIE_PAIR = re.compile(r"\s*([^=;]+)\s*=\s*([^;]*)")
|
||||
|
|
|
|||
|
|
@ -2103,6 +2103,71 @@ def _load_social_i18n(lang: str) -> dict:
|
|||
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)
|
||||
async def social_view(token: str, request: Request) -> HTMLResponse:
|
||||
"""Per-client social mapping HTML page.
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import __version__, social, store, threat_intel
|
||||
from .api import router as toolbox_router
|
||||
|
|
@ -26,6 +28,14 @@ app = FastAPI(
|
|||
)
|
||||
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")
|
||||
async def _startup() -> None:
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@
|
|||
}
|
||||
async function confirmWipe() {
|
||||
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);
|
||||
const j = await r.json();
|
||||
wipeModal.close();
|
||||
|
|
@ -226,7 +226,7 @@
|
|||
// ─── fetch + bootstrap ───
|
||||
async function fetchGraph() {
|
||||
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);
|
||||
const g = await r.json();
|
||||
render(g);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user