Compare commits

...

2 Commits

Author SHA1 Message Date
CyberMind
febf58fd27
Merge pull request #520 from CyberMind-FR/feature/518-phase-12-c-operator-grade-state-adjacent
Some checks are pending
License Headers / check (push) Waiting to run
Phase 12.C — operator-grade / state-adjacent identity detection (#518)
2026-06-10 14:51:58 +02:00
0d1d3475db feat(toolbox): Phase 12.C — operator-grade / state-adjacent identity detection (ref #518)
Extends Phase 8 Utiq (#500) into a general top-severity identity detector.
Passive detection only — no bypass.

  - social_graph.py detect_operator_grade(): telco header-enrichment
    (MSISDN/x-acr/WAP-bearer), operator-consortium IDs (Utiq/TrustPid),
    data-broker / state-adjacent hosts (LiveRamp/BlueKai/Acxiom/Neustar/
    Tapad/Experian/Palantir-class). Returns (vendor, category).
  - social.py: social_host_meta.opgrade_vendor+category; social_opgrade
    per-client table; record_host_opgrade + record_opgrade_event +
    opgrade_for_client; fetch_graph/aggregate/wipe wired.
  - social.js: top-tier void-purple lens (above anti-bot) + pulsing
    double ring +  banner; node-detail vendor+category.
  - index.html operator tab: opgrade breakdown card.
  - social_report.py: operator-grade evidence section in the PDF.
  - i18n FR/EN + cache-bust ?v=26c. changelog 2.6.7.

Live on gk2: social_opgrade table created, detect matrix green
(msisdn/x-acr/utiq/liveramp/bluekai/palantir/none), aggregate by_opgrade
+ opgrade_clients present, per-client stats populate, PDF 42777 valid.
Factual network-surface flagging only, no entity qualification.
2026-06-10 14:36:02 +02:00
10 changed files with 322 additions and 13 deletions

View File

@ -33,5 +33,7 @@
"card_evidence_active": "Compliance analysis: trackers before consent (GDPR art. 6.1.a + 7) and extra-EU transfers (art. 44). See the PDF report for details.", "card_evidence_active": "Compliance analysis: trackers before consent (GDPR art. 6.1.a + 7) and extra-EU transfers (art. 44). See the PDF report for details.",
"node_detail_cdn": "CDN / cache", "node_detail_cdn": "CDN / cache",
"node_detail_antibot": "Anti-bot", "node_detail_antibot": "Anti-bot",
"antibot_alert": "⚠ {n} site(s) challenged that you are human" "antibot_alert": "⚠ {n} site(s) challenged that you are human",
"node_detail_opgrade": "Operator / state",
"opgrade_alert": "⛔ {n} operator-grade / state-adjacent surface(s)"
} }

View File

@ -33,5 +33,7 @@
"card_evidence_active": "Analyse de conformité : traqueurs avant consentement (RGPD art. 6.1.a + 7) et transferts hors UE (art. 44). Voir le rapport PDF pour le détail.", "card_evidence_active": "Analyse de conformité : traqueurs avant consentement (RGPD art. 6.1.a + 7) et transferts hors UE (art. 44). Voir le rapport PDF pour le détail.",
"node_detail_cdn": "CDN / cache", "node_detail_cdn": "CDN / cache",
"node_detail_antibot": "Anti-bot", "node_detail_antibot": "Anti-bot",
"antibot_alert": "⚠ {n} site(s) ont vérifié que vous êtes humain" "antibot_alert": "⚠ {n} site(s) ont vérifié que vous êtes humain",
"node_detail_opgrade": "Opérateur / état",
"opgrade_alert": "⛔ {n} surface(s) opérateur-grade / proche-État"
} }

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>{{ t.page_title }} — VILLAGE3B</title> <title>{{ t.page_title }} — VILLAGE3B</title>
<link rel="stylesheet" href="/toolbox/social.css?v=264b"> <link rel="stylesheet" href="/toolbox/social.css?v=26c">
<script> <script>
// Inline JSON safer than a data-* attribute : FR text has // Inline JSON safer than a data-* attribute : FR text has
// apostrophes (l'effacement) that would break single-quoted // apostrophes (l'effacement) that would break single-quoted
@ -14,7 +14,7 @@
window.__SOCIAL_I18N__ = {{ t_json | safe }}; window.__SOCIAL_I18N__ = {{ t_json | safe }};
</script> </script>
<script defer src="/toolbox/d3.v7.min.js"></script> <script defer src="/toolbox/d3.v7.min.js"></script>
<script defer src="/toolbox/social.js?v=264b"></script> <script defer src="/toolbox/social.js?v=26c"></script>
</head> </head>
<body data-token="{{ token }}" data-lang="{{ lang }}"> <body data-token="{{ token }}" data-lang="{{ lang }}">
@ -34,6 +34,7 @@
<div class="stat-tile"><span class="stat-n" data-bind="total_sites">0</span><span class="stat-l">{{ t.stats_total_sites }}</span></div> <div class="stat-tile"><span class="stat-n" data-bind="total_sites">0</span><span class="stat-l">{{ t.stats_total_sites }}</span></div>
</section> </section>
<p class="graph-hint">{{ t.graph_swipe_hint }}</p> <p class="graph-hint">{{ t.graph_swipe_hint }}</p>
<div id="opgrade-alert" class="opgrade-alert" hidden></div>
<div id="antibot-alert" class="antibot-alert" hidden></div> <div id="antibot-alert" class="antibot-alert" hidden></div>
<svg id="social-graph" role="img" preserveAspectRatio="xMidYMid meet"></svg> <svg id="social-graph" role="img" preserveAspectRatio="xMidYMid meet"></svg>
</section> </section>
@ -52,6 +53,8 @@
<dd data-bind="nd_cdn">—</dd> <dd data-bind="nd_cdn">—</dd>
<dt>{{ t.node_detail_antibot }}</dt> <dt>{{ t.node_detail_antibot }}</dt>
<dd data-bind="nd_antibot">—</dd> <dd data-bind="nd_antibot">—</dd>
<dt>{{ t.node_detail_opgrade }}</dt>
<dd data-bind="nd_opgrade">—</dd>
<dt>{{ t.node_detail_sites }}</dt> <dt>{{ t.node_detail_sites }}</dt>
<dd data-bind="nd_sites">—</dd> <dd data-bind="nd_sites">—</dd>
<dt>{{ t.node_detail_first_seen }}</dt> <dt>{{ t.node_detail_first_seen }}</dt>

View File

@ -1,3 +1,29 @@
secubox-toolbox (2.6.7-1~bookworm1) bookworm; urgency=medium
* Phase 12.C (#518, parent #514) — operator-grade / state-adjacent
identity detection (extends Utiq #500). Passive, top-severity tier.
- social_graph.py detect_operator_grade(): telco header-enrichment
(MSISDN / x-acr / WAP bearer), operator-consortium IDs (Utiq /
TrustPid), and data-broker / state-adjacent analytics hosts
(LiveRamp / Oracle-BlueKai / Acxiom / Neustar / Tapad / Experian
/ Palantir-class). Returns (vendor, category) for severity tier.
- social.py: social_host_meta.opgrade_vendor + opgrade_category;
new social_opgrade per-client table; record_host_opgrade +
record_opgrade_event + opgrade_for_client; fetch_graph carries
it + stats; aggregate by_opgrade + opgrade_clients; wipe_mac
clears social_opgrade.
- social.js: top-tier void-purple lens (above anti-bot cinnabar)
+ pulsing double ring + ⛔ "operator-grade / state-adjacent"
banner; node-detail shows vendor + category.
- index.html operator tab: opérateur-grade breakdown card.
- social_report.py: operator-grade evidence section in the PDF.
- i18n: node_detail_opgrade + opgrade_alert (FR/EN). Assets
cache-busted ?v=26c.
Detection only — no bypass. Factual network-surface flagging, no
claim about any specific entity.
-- Gerald KERMA <devel@cybermind.fr> Wed, 10 Jun 2026 12:00:00 +0200
secubox-toolbox (2.6.6-1~bookworm1) bookworm; urgency=medium secubox-toolbox (2.6.6-1~bookworm1) bookworm; urgency=medium
* Phase 12.B follow-up (#516) — fix the Clients-tab 🕸️ Carto link * Phase 12.B follow-up (#516) — fix the Clients-tab 🕸️ Carto link

View File

@ -382,6 +382,59 @@ def detect_antibot(flow) -> Optional[str]:
return None return None
# ─── operator-grade / state-adjacent identity detection (Phase 12.C #518) ──
# The heaviest identity surfaces : carrier header-enrichment (the network
# itself injects your phone number / subscriber id), operator-consortium
# IDs (Utiq / TrustPid), and known data-broker / state-adjacent analytics
# aggregation endpoints. We flag the network SURFACE factually — never a
# claim about a specific entity. Returns (vendor, category) or (None, None).
# category ∈ {telco-enrichment, consortium, data-broker} for severity tiering.
_OPGRADE_HEADERS = ( # carrier-injected request headers
("telco-enrichment", "MSISDN-header", (
"x-msisdn", "x-up-calling-line-id", "x-nokia-msisdn", "x-wap-msisdn",
"x-up-subno", "msisdn", "x-forwarded-for-msisdn", "x-network-info")),
("telco-enrichment", "Verizon/AT&T-ACR", ("x-acr",)),
("telco-enrichment", "WAP-bearer", ("x-up-bearer-type",)),
("consortium", "TrustPid", ("x-trustpid",)),
)
_OPGRADE_HOSTS = ( # host eTLD+1 fragments
("consortium", "Utiq", ("utiq.com", "consenthub.utiq")),
("consortium", "TrustPid", ("trustpid.com", "trustpid")),
("data-broker", "LiveRamp", ("rlcdn.com", "liveramp.com", "liveramp")),
("data-broker", "Oracle-BlueKai", ("bluekai.com", "bkrtx.com")),
("data-broker", "Acxiom", ("acxiom.com", "acxiom-online")),
("data-broker", "Neustar", ("neustar.biz", "agkn.com")),
("data-broker", "Tapad", ("tapad.com",)),
("data-broker", "Experian", ("experian.com", "tapestry.experian")),
("data-broker", "Palantir-class", ("palantir.com", "palantircloud", "foundry.palantir")),
)
def detect_operator_grade(flow) -> tuple:
"""Return (vendor, category) for an operator-grade / state-adjacent
identity surface, or (None, None). Passive ; factual.
"""
try:
# 1) Carrier-injected request headers (the strongest signal —
# the NETWORK enriched the request with subscriber identity).
try:
keys = " ".join(k.lower() for k in flow.request.headers.keys())
except Exception:
keys = ""
for category, vendor, hdrs in _OPGRADE_HEADERS:
if any(h in keys for h in hdrs):
return vendor, category
# 2) Host-based : consortium IDs + data-broker / state-adjacent.
host = (flow.request.host or "").lower()
for category, vendor, frags in _OPGRADE_HOSTS:
if any(f in host for f in frags):
return vendor, category
except Exception:
pass
return None, None
# ─── JA4 lookup ─── # ─── JA4 lookup ───
def _ja4_hash(flow) -> Optional[str]: def _ja4_hash(flow) -> Optional[str]:
"""Pull the JA4 fingerprint set by the ja4 addon, if present.""" """Pull the JA4 fingerprint set by the ja4 addon, if present."""
@ -449,6 +502,18 @@ class SocialGraph:
client_mac_hash=mac_hash, src_site=src_site, client_mac_hash=mac_hash, src_site=src_site,
antibot_vendor=antibot, antibot_vendor=antibot,
) )
# Phase 12.C (#518) — operator-grade / state-adjacent identity.
op_vendor, op_category = detect_operator_grade(flow)
if op_vendor:
if resp_host:
_social.record_host_opgrade(
domain=resp_host, opgrade_vendor=op_vendor,
opgrade_category=op_category,
)
_social.record_opgrade_event(
client_mac_hash=mac_hash, src_site=src_site,
opgrade_vendor=op_vendor, category=op_category,
)
except Exception: except Exception:
pass pass

View File

@ -112,13 +112,16 @@ CREATE TABLE IF NOT EXISTS social_links (
); );
-- Phase 12.A (#515) — host-stable CDN/edge metadata, mac-independent. -- Phase 12.A (#515) — host-stable CDN/edge metadata, mac-independent.
-- Phase 12.B (#516) adds antibot_vendor (anti-bot / CAPTCHA vendor). -- Phase 12.B (#516) adds antibot_vendor. Phase 12.C (#518) adds
-- opgrade_vendor + opgrade_category (operator-grade / state-adjacent).
CREATE TABLE IF NOT EXISTS social_host_meta ( CREATE TABLE IF NOT EXISTS social_host_meta (
tracker_domain TEXT PRIMARY KEY, tracker_domain TEXT PRIMARY KEY,
cdn_vendor TEXT, cdn_vendor TEXT,
cache_status TEXT, cache_status TEXT,
antibot_vendor TEXT, antibot_vendor TEXT,
last_seen INTEGER NOT NULL opgrade_vendor TEXT,
opgrade_category TEXT,
last_seen INTEGER NOT NULL
); );
-- Phase 12.B (#516) — per-client "challenged your humanity" log. -- Phase 12.B (#516) — per-client "challenged your humanity" log.
@ -130,6 +133,17 @@ CREATE TABLE IF NOT EXISTS social_antibot (
last_seen INTEGER NOT NULL, last_seen INTEGER NOT NULL,
PRIMARY KEY (client_mac_hash, src_site, antibot_vendor) PRIMARY KEY (client_mac_hash, src_site, antibot_vendor)
); );
-- Phase 12.C (#518) — per-client operator-grade / state-adjacent log.
CREATE TABLE IF NOT EXISTS social_opgrade (
client_mac_hash TEXT NOT NULL,
src_site TEXT NOT NULL,
opgrade_vendor TEXT NOT NULL,
category TEXT NOT NULL,
hits INTEGER NOT NULL DEFAULT 0,
last_seen INTEGER NOT NULL,
PRIMARY KEY (client_mac_hash, src_site, opgrade_vendor)
);
""" """
@ -156,6 +170,9 @@ _PHASE11C_MIGRATIONS = (
# Phase 12.B (#516) — anti-bot vendor column on the host-meta table # Phase 12.B (#516) — anti-bot vendor column on the host-meta table
# (created by 12.A ; additive on a 2.6.3/2.6.4 → upgrade). # (created by 12.A ; additive on a 2.6.3/2.6.4 → upgrade).
("social_host_meta", "antibot_vendor", "TEXT"), ("social_host_meta", "antibot_vendor", "TEXT"),
# Phase 12.C (#518) — operator-grade / state-adjacent columns.
("social_host_meta", "opgrade_vendor", "TEXT"),
("social_host_meta", "opgrade_category", "TEXT"),
) )
@ -412,6 +429,83 @@ def antibot_for_client(mac_hash: str, since_seconds: int = 86400) -> List[Dict]:
return out return out
# ── Phase 12.C (#518) — operator-grade / state-adjacent recording ──
def _record_host_opgrade_sync(domain, vendor, category) -> None:
try:
with _conn() as c:
c.execute(
"INSERT INTO social_host_meta(tracker_domain, opgrade_vendor, "
"opgrade_category, last_seen) VALUES (?, ?, ?, ?) "
"ON CONFLICT(tracker_domain) DO UPDATE SET "
"opgrade_vendor=excluded.opgrade_vendor, "
"opgrade_category=excluded.opgrade_category, "
"last_seen=excluded.last_seen",
(domain, vendor, category, int(time.time())),
)
except Exception as e: # pragma: no cover
log.warning("record_host_opgrade failed: %s", e)
def record_host_opgrade(*, domain: str, opgrade_vendor: str,
opgrade_category: Optional[str] = None) -> None:
if not (domain and opgrade_vendor):
return
try:
_executor.submit(_record_host_opgrade_sync, domain, opgrade_vendor,
opgrade_category)
except RuntimeError:
pass
def _record_opgrade_event_sync(mac_hash, src_site, vendor, category) -> None:
try:
with _conn() as c:
c.execute(
"INSERT INTO social_opgrade(client_mac_hash, src_site, "
"opgrade_vendor, category, hits, last_seen) "
"VALUES (?, ?, ?, ?, 1, ?) "
"ON CONFLICT(client_mac_hash, src_site, opgrade_vendor) DO UPDATE "
"SET hits = hits + 1, category = excluded.category, "
"last_seen = excluded.last_seen",
(mac_hash, src_site, vendor, category, int(time.time())),
)
except Exception as e: # pragma: no cover
log.warning("record_opgrade_event failed: %s", e)
def record_opgrade_event(*, client_mac_hash: str, src_site: str,
opgrade_vendor: str, category: str) -> None:
if not (client_mac_hash and src_site and opgrade_vendor):
return
try:
_executor.submit(_record_opgrade_event_sync, client_mac_hash,
src_site, opgrade_vendor, category or "")
except RuntimeError:
pass
def opgrade_for_client(mac_hash: str, since_seconds: int = 86400) -> List[Dict]:
"""Per-client operator-grade / state-adjacent list for the alert tile
+ the PDF evidence section."""
since = int(time.time()) - max(since_seconds, 3600)
out: List[Dict] = []
if not mac_hash:
return out
try:
with _conn() as c:
for r in c.execute(
"SELECT src_site, opgrade_vendor, category, hits, last_seen "
"FROM social_opgrade WHERE client_mac_hash = ? AND last_seen >= ? "
"ORDER BY last_seen DESC LIMIT 100",
(mac_hash, since),
).fetchall():
out.append(dict(r))
except Exception as e: # pragma: no cover
log.warning("opgrade_for_client failed: %s", e)
return out
def fold_recent(window_seconds: int = 300) -> Tuple[int, int]: def fold_recent(window_seconds: int = 300) -> Tuple[int, int]:
"""Fold raw edges from the last `window_seconds` into the node + link """Fold raw edges from the last `window_seconds` into the node + link
aggregate tables. Returns (nodes_touched, links_touched). aggregate tables. Returns (nodes_touched, links_touched).
@ -606,7 +700,8 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
# can colour/label by edge-network vendor. # can colour/label by edge-network vendor.
for r in c.execute( for r in c.execute(
"SELECT n.tracker_domain, n.hits, n.first_seen, n.last_seen, " "SELECT n.tracker_domain, n.hits, n.first_seen, n.last_seen, "
"n.sites_jsonl, m.cdn_vendor, m.cache_status, m.antibot_vendor " "n.sites_jsonl, m.cdn_vendor, m.cache_status, m.antibot_vendor, "
"m.opgrade_vendor, m.opgrade_category "
"FROM social_nodes n " "FROM social_nodes n "
"LEFT JOIN social_host_meta m ON m.tracker_domain = n.tracker_domain " "LEFT JOIN social_host_meta m ON m.tracker_domain = n.tracker_domain "
"WHERE n.client_mac_hash = ? AND n.last_seen >= ? " "WHERE n.client_mac_hash = ? AND n.last_seen >= ? "
@ -629,6 +724,8 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
"cdn_vendor": r["cdn_vendor"], "cdn_vendor": r["cdn_vendor"],
"cache_status": r["cache_status"], "cache_status": r["cache_status"],
"antibot_vendor": r["antibot_vendor"], "antibot_vendor": r["antibot_vendor"],
"opgrade_vendor": r["opgrade_vendor"],
"opgrade_category": r["opgrade_category"],
} }
) )
@ -670,6 +767,9 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
# Phase 12.B — per-client anti-bot challenges for the alert tile. # Phase 12.B — per-client anti-bot challenges for the alert tile.
antibot = antibot_for_client(mac_hash, since_seconds=since_seconds) antibot = antibot_for_client(mac_hash, since_seconds=since_seconds)
out["antibot"] = antibot out["antibot"] = antibot
# Phase 12.C — operator-grade / state-adjacent surfaces.
opgrade = opgrade_for_client(mac_hash, since_seconds=since_seconds)
out["opgrade"] = opgrade
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,
@ -677,6 +777,8 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
"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}),
"antibot_vendors": sorted({a["antibot_vendor"] for a in antibot}), "antibot_vendors": sorted({a["antibot_vendor"] for a in antibot}),
"opgrade_sites": len({o["src_site"] for o in opgrade}),
"opgrade_vendors": sorted({o["opgrade_vendor"] for o in opgrade}),
} }
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
log.warning("fetch_graph failed: %s", e) log.warning("fetch_graph failed: %s", e)
@ -694,7 +796,7 @@ def wipe_mac(mac_hash: str) -> int:
try: try:
with _conn() as c: with _conn() as c:
for table in ("social_edges", "social_nodes", "social_links", for table in ("social_edges", "social_nodes", "social_links",
"social_antibot"): "social_antibot", "social_opgrade"):
cur = c.execute( cur = c.execute(
f"DELETE FROM {table} WHERE client_mac_hash = ?", (mac_hash,) f"DELETE FROM {table} WHERE client_mac_hash = ?", (mac_hash,)
) )
@ -721,6 +823,8 @@ def aggregate(hours: int = 24) -> Dict:
"by_cdn": [], # Phase 12.A "by_cdn": [], # Phase 12.A
"by_antibot": [], # Phase 12.B "by_antibot": [], # Phase 12.B
"antibot_clients": 0, # Phase 12.B "antibot_clients": 0, # Phase 12.B
"by_opgrade": [], # Phase 12.C
"opgrade_clients": 0, # Phase 12.C
} }
try: try:
with _conn() as c: with _conn() as c:
@ -787,6 +891,24 @@ def aggregate(hours: int = 24) -> Dict:
"WHERE last_seen >= ?", "WHERE last_seen >= ?",
(since,), (since,),
).fetchone()[0] ).fetchone()[0]
# Phase 12.C — operator-grade / state-adjacent breakdown.
out["by_opgrade"] = [
dict(r)
for r in c.execute(
"SELECT opgrade_vendor, category, "
"COUNT(DISTINCT src_site) AS sites, "
"COUNT(DISTINCT client_mac_hash) AS clients, "
"SUM(hits) AS events "
"FROM social_opgrade WHERE last_seen >= ? "
"GROUP BY opgrade_vendor ORDER BY events DESC LIMIT 25",
(since,),
).fetchall()
]
out["opgrade_clients"] = c.execute(
"SELECT COUNT(DISTINCT client_mac_hash) FROM social_opgrade "
"WHERE last_seen >= ?",
(since,),
).fetchone()[0]
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
log.warning("aggregate failed: %s", e) log.warning("aggregate failed: %s", e)
return out return out

View File

@ -49,15 +49,28 @@ _L = {
"Nous rapportons le fait observé, pas l'absence de garanties (SCC).", "Nous rapportons le fait observé, pas l'absence de garanties (SCC).",
"Legal basis: GDPR art. 44+ (international transfers). We report the " "Legal basis: GDPR art. 44+ (international transfers). We report the "
"observed fact, not the absence of safeguards (SCC)."), "observed fact, not the absence of safeguards (SCC)."),
"ev_opgrade": ("Évidence : surfaces opérateur-grade / proche-État",
"Evidence: operator-grade / state-adjacent surfaces"),
"ev_opgrade_basis": ("Enrichissement réseau (MSISDN/ACR), identifiants de "
"consortium opérateur, et courtiers de données / analytics "
"proches-État. Fait réseau observé, sans qualification d'entité.",
"Network enrichment (MSISDN/ACR), operator-consortium IDs, and "
"data-broker / state-adjacent analytics. Observed network fact, "
"no entity qualification."),
"col_tracker": ("Traqueur", "Tracker"), "col_tracker": ("Traqueur", "Tracker"),
"col_sites": ("Sites", "Sites"), "col_sites": ("Sites", "Sites"),
"col_hits": ("Occurrences", "Hits"), "col_hits": ("Occurrences", "Hits"),
"col_country": ("Pays", "Country"), "col_country": ("Pays", "Country"),
"col_asn": ("Hébergeur (ASN)", "Host (ASN)"), "col_asn": ("Hébergeur (ASN)", "Host (ASN)"),
"col_opvendor": ("Surface", "Surface"),
"col_opcat": ("Catégorie", "Category"),
"col_site": ("Site", "Site"),
"none_pre": ("Aucun traqueur déclenché avant consentement détecté.", "none_pre": ("Aucun traqueur déclenché avant consentement détecté.",
"No tracker fired before consent detected."), "No tracker fired before consent detected."),
"none_eu": ("Aucun transfert hors UE/EEE détecté.", "none_eu": ("Aucun transfert hors UE/EEE détecté.",
"No extra-EU/EEA transfer detected."), "No extra-EU/EEA transfer detected."),
"none_op": ("Aucune surface opérateur-grade / proche-État détectée.",
"No operator-grade / state-adjacent surface detected."),
"footer": ("Rapport factuel — données calculées localement, aucune donnée externe. " "footer": ("Rapport factuel — données calculées localement, aucune donnée externe. "
"Anonyme : aucune valeur de cookie brute conservée. Droit à l'effacement : RGPD art. 17.", "Anonyme : aucune valeur de cookie brute conservée. Droit à l'effacement : RGPD art. 17.",
"Factual report — computed locally, no external data. Anonymous: no raw cookie " "Factual report — computed locally, no external data. Anonymous: no raw cookie "
@ -87,6 +100,8 @@ def build_social_report(mac_hash: str, since_seconds: int = 7 * 86400) -> Dict:
"total_sites": stats.get("total_sites", 0), "total_sites": stats.get("total_sites", 0),
"pre_consent": evidence.get("pre_consent", []), "pre_consent": evidence.get("pre_consent", []),
"extra_eu": evidence.get("extra_eu", []), "extra_eu": evidence.get("extra_eu", []),
# Phase 12.C — operator-grade / state-adjacent surfaces.
"opgrade": _social.opgrade_for_client(mac_hash, since_seconds=since_seconds),
} }
@ -181,6 +196,21 @@ def render_social_pdf(report: Dict) -> bytes:
]) ])
else: else:
_note(pdf, family, _bi("none_eu")) _note(pdf, family, _bi("none_eu"))
pdf.ln(3)
# ── Evidence : operator-grade / state-adjacent (Phase 12.C) ──
bi("ev_opgrade", 12, "B", 110, 64, 201, gap=0.3)
bi("ev_opgrade_basis", 8, "", 120, 120, 120, gap=1)
op = report.get("opgrade", [])
if op:
_table(pdf, family, op, [
(_bi("col_opvendor"), "opgrade_vendor", 45),
(_bi("col_opcat"), "category", 40),
(_bi("col_site"), "src_site", 55),
(_bi("col_hits"), "hits", 25),
])
else:
_note(pdf, family, _bi("none_op"))
pdf.ln(4) pdf.ln(4)
# ── Footer ── # ── Footer ──

View File

@ -144,6 +144,10 @@
<h2>🤖 Anti-bot / "prouvez que vous êtes humain"</h2> <h2>🤖 Anti-bot / "prouvez que vous êtes humain"</h2>
<div id="social-antibot"><div class="empty">loading…</div></div> <div id="social-antibot"><div class="empty">loading…</div></div>
</div> </div>
<div class="card">
<h2>📡 Opérateur-grade / proche-État</h2>
<div id="social-opgrade"><div class="empty">loading…</div></div>
</div>
<div class="card" style="grid-column:1/-1"> <div class="card" style="grid-column:1/-1">
<h2>🎯 Top tracker domains</h2> <h2>🎯 Top tracker domains</h2>
<div id="social-trackers"><div class="empty">loading…</div></div> <div id="social-trackers"><div class="empty">loading…</div></div>
@ -405,6 +409,16 @@ async function loadSocial() {
'</tbody></table>' '</tbody></table>'
: '<div class="empty">aucun anti-bot détecté</div>'; : '<div class="empty">aucun anti-bot détecté</div>';
} }
const og = document.getElementById('social-opgrade');
if (og) {
const bo = agg.by_opgrade || [];
og.innerHTML = bo.length
? `<p style="font-size:0.8rem;color:var(--p31-dim);margin-bottom:0.5rem">${agg.opgrade_clients||0} client(s) exposé(s)</p>` +
'<table><thead><tr><th>Vendor</th><th>catégorie</th><th>sites</th><th>clients</th><th>events</th></tr></thead><tbody>' +
bo.map(r => `<tr><td>📡 ${r.opgrade_vendor}</td><td>${r.category}</td><td>${r.sites}</td><td>${r.clients}</td><td>${r.events}</td></tr>`).join('') +
'</tbody></table>'
: '<div class="empty">aucune surface opérateur-grade détectée</div>';
}
} }
async function refreshAll() { async function refreshAll() {

View File

@ -327,3 +327,27 @@ dialog h2 {
.ring-guides circle { fill: none; stroke-width: 1; pointer-events: none; } .ring-guides circle { fill: none; stroke-width: 1; pointer-events: none; }
.ring-inner { stroke: var(--gold-hermetic); stroke-opacity: .18; stroke-dasharray: 2 4; } .ring-inner { stroke: var(--gold-hermetic); stroke-opacity: .18; stroke-dasharray: 2 4; }
.ring-outer { stroke: var(--cyber-cyan); stroke-opacity: .14; stroke-dasharray: 2 6; } .ring-outer { stroke: var(--cyber-cyan); stroke-opacity: .14; stroke-dasharray: 2 6; }
/* ── Operator-grade / state-adjacent (Phase 12.C) — top severity ── */
.opgrade-ring {
fill: none;
stroke: var(--void-purple);
stroke-width: 2;
opacity: .9;
animation: opgrade-pulse 1.8s ease-in-out infinite;
transform-origin: center;
transform-box: fill-box;
}
@keyframes opgrade-pulse { 0%,100% { r: 13px; opacity: .5; } 50% { r: 17px; opacity: 1; } }
.opgrade-alert {
position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
z-index: 7;
background: var(--void-purple);
color: #fff;
font-family: 'JetBrains Mono', monospace;
font-size: 11px; font-weight: bold;
padding: 5px 12px; border-radius: 3px;
max-width: 90%; text-align: center;
box-shadow: 0 2px 12px rgba(110,64,201,.5);
}
.opgrade-alert + .antibot-alert { top: 38px; }

View File

@ -58,6 +58,16 @@
el.hidden = false; el.hidden = false;
} }
// Phase 12.C — top-severity operator-grade / state-adjacent banner.
function updateOpgradeTile(sites, vendors) {
const el = document.getElementById('opgrade-alert');
if (!el) return;
if (!sites) { el.hidden = true; return; }
const v = (vendors || []).join(', ');
el.textContent = t('opgrade_alert', { n: sites }) + (v ? ' — ' + v : '');
el.hidden = false;
}
// ─── graph state ─── // ─── graph state ───
let simulation = null; let simulation = null;
@ -83,6 +93,7 @@
bind('total_sites', graph.stats.total_sites || 0); bind('total_sites', graph.stats.total_sites || 0);
// Phase 12.B — "challenged your humanity" alert tile. // Phase 12.B — "challenged your humanity" alert tile.
updateAntibotTile(graph.stats.antibot_sites || 0, graph.stats.antibot_vendors || []); updateAntibotTile(graph.stats.antibot_sites || 0, graph.stats.antibot_vendors || []);
updateOpgradeTile(graph.stats.opgrade_sites || 0, graph.stats.opgrade_vendors || []);
// Empty graph → just return ; the stats tiles already show 0/0 and // Empty graph → just return ; the stats tiles already show 0/0 and
// the user knows. No persistent overlay message. // the user knows. No persistent overlay message.
@ -122,6 +133,8 @@
cdn_vendor: n.cdn_vendor || null, cdn_vendor: n.cdn_vendor || null,
cache_status: n.cache_status || null, cache_status: n.cache_status || null,
antibot_vendor: n.antibot_vendor || null, antibot_vendor: n.antibot_vendor || null,
opgrade_vendor: n.opgrade_vendor || null,
opgrade_category: n.opgrade_category || null,
}); });
} }
@ -214,7 +227,10 @@
function nodeColor(d) { function nodeColor(d) {
if (d.kind === 'eye') return 'var(--cinnabar)'; if (d.kind === 'eye') return 'var(--cinnabar)';
if (d.kind === 'site') return 'var(--gold-hermetic)'; if (d.kind === 'site') return 'var(--gold-hermetic)';
// Phase 12.B — anti-bot hosts get the highest-severity lens. // Severity tiers (highest first):
// 12.C operator-grade / state-adjacent = void-purple (top).
if (d.opgrade_vendor) return 'var(--void-purple)';
// 12.B anti-bot = cinnabar.
if (d.antibot_vendor) return 'var(--cinnabar)'; if (d.antibot_vendor) return 'var(--cinnabar)';
if (d.cdn_vendor && CDN_COLORS[d.cdn_vendor]) return CDN_COLORS[d.cdn_vendor]; if (d.cdn_vendor && CDN_COLORS[d.cdn_vendor]) return CDN_COLORS[d.cdn_vendor];
return 'var(--cyber-cyan)'; return 'var(--cyber-cyan)';
@ -231,6 +247,10 @@
nodeG.filter(d => d.kind === 'tracker' && d.antibot_vendor) nodeG.filter(d => d.kind === 'tracker' && d.antibot_vendor)
.append('circle').attr('class', 'antibot-ring').attr('r', 12); .append('circle').attr('class', 'antibot-ring').attr('r', 12);
// Phase 12.C — operator-grade hosts get the top-tier double ring.
nodeG.filter(d => d.kind === 'tracker' && d.opgrade_vendor)
.append('circle').attr('class', 'opgrade-ring').attr('r', 14);
// Site + tracker nodes. // Site + tracker nodes.
nodeG.filter(d => d.kind !== 'eye').append('circle') nodeG.filter(d => d.kind !== 'eye').append('circle')
.attr('r', d => d.kind === 'tracker' ? 7 : 10) .attr('r', d => d.kind === 'tracker' ? 7 : 10)
@ -327,6 +347,7 @@
bind('nd_asn', '—'); bind('nd_asn', '—');
bind('nd_cdn', node.cdn_vendor ? (node.cdn_vendor + (node.cache_status ? ' · ' + node.cache_status : '')) : '—'); bind('nd_cdn', node.cdn_vendor ? (node.cdn_vendor + (node.cache_status ? ' · ' + node.cache_status : '')) : '—');
bind('nd_antibot', node.antibot_vendor ? ('🤖 ' + node.antibot_vendor) : '—'); bind('nd_antibot', node.antibot_vendor ? ('🤖 ' + node.antibot_vendor) : '—');
bind('nd_opgrade', node.opgrade_vendor ? ('📡 ' + node.opgrade_vendor + (node.opgrade_category ? ' (' + node.opgrade_category + ')' : '')) : '—');
bind('nd_sites', (node.sites || []).join(', ') || '—'); bind('nd_sites', (node.sites || []).join(', ') || '—');
bind('nd_first_seen', node.first_seen ? new Date(node.first_seen * 1000).toISOString().slice(0, 16).replace('T', ' ') : '—'); bind('nd_first_seen', node.first_seen ? new Date(node.first_seen * 1000).toISOString().slice(0, 16).replace('T', ' ') : '—');
bind('nd_last_seen', node.last_seen ? new Date(node.last_seen * 1000).toISOString().slice(0, 16).replace('T', ' ') : '—'); bind('nd_last_seen', node.last_seen ? new Date(node.last_seen * 1000).toISOString().slice(0, 16).replace('T', ' ') : '—');