secubox-deb/common/secubox_core/classifiers/avatar.py
CyberMind-FR e67baf4cd7 feat(secubox-core+5modules+toolbox): Phase 2c receiving modules enrichment (ref #490)
Phase 2b (#488/#489) wired mitm addons to 5 receiving modules but events
were persisted raw. This phase implements actual enrichment in each module
via the enrich_hook param, plus aggregator merge so reports show meaningful
data (YouTube/Signal/iPhone/Safari/JA4 fingerprints) instead of raw bytes.

  - host_app.py  : 60+ host patterns -> app + category + emoji
  - cookie.py    : 40+ cookie tracker patterns -> provider + category + emoji
  - avatar.py    : UA + Client Hints -> device + browser + OS + emoji
  - ja4.py       : NEW. Deterministic JA4-style fingerprint hash from
                   cipher_suites + alpn + extensions. Lookup table for
                   known JA4 fingerprints (empty for now, Phase 3 will
                   populate). 12-char hex (SHA256 truncated).

The host_app/cookie/avatar are copies of the secubox-toolbox classifiers
moved to secubox-core so all 5 receiving modules can import them. The
secubox-toolbox-local ones stay as-is to avoid breaking changes — Phase 3
will consolidate.

  - secubox-dpi             : host/SNI -> {app, category, emoji}
  - secubox-cookies         : cookie names -> {providers{}, categories{}}
  - secubox-avatar          : UA + CH -> {device, browser, os_label}
  - secubox-threat-analyst  : ClientHello -> {ja4_fingerprint, known_client}
  - secubox-soc             : indicators -> {total_weight, band, kinds}

Each is ~25 lines, called by mount_ingest_routes BEFORE persistence.
Enriched output joins the raw event under the 'enriched' key.

_pull_mitm_module_events() now also calls _summarize_enriched(kind, events)
to consolidate per-module enrichment :

  - dpi             : top_apps[] aggregated from enriched.app counts
  - cookies         : top_providers[] from enriched.providers + tracker_total
  - avatar          : devices{} + browsers{} from enriched.{device,browser}
  - threat-analyst  : top_fingerprints[] grouped by JA4 hash
  - soc             : total_weight + max_band + indicator_kinds

These appear under mitm_modules.<kind>.enriched_summary in the /report JSON.

POST realistic payloads to all 5 sockets :
  - YouTube host -> dpi/enriched.app = 'YouTube' (streaming)
  - GA + FB Pixel cookies -> cookies/providers : GA x3, FB x1, total 4
  - iPhone Safari UA -> avatar/device='iPhone' (📱 iOS 17.4) + Safari (🧭)
  - facebook.com ClientHello -> threat-analyst/ja4 = '7175ee3a68f0'
  - 2 indicators (weight 15+25) -> soc/band='medium', total=40, kinds=[dga, suspicious]

  - secubox-dpi : call live nDPI/netifyd socket (currently pattern-match only)
  - secubox-threat-analyst : implement full FoxIO JA4 string format (currently
    deterministic SHA256 trunc which is JA4-like but not the canonical format)
  - secubox-soc : threat-intel feed lookup (currently just sums static weights)
  - secubox-avatar : screen/timing fingerprinting via WebGL hash (currently UA only)
  - Reports (PDF + HTML) : surface mitm_modules.enriched_summary in the report UI
2026-06-13 08:00:18 +02:00

117 lines
5.0 KiB
Python

# 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),
}