mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-01 09:26:16 +00:00
Compare commits
2 Commits
66b608d760
...
febf58fd27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
febf58fd27 | ||
| 0d1d3475db |
|
|
@ -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)"
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ──
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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', ' ') : '—');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user