mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 19:16:07 +00:00
Compare commits
No commits in common. "92b203cbbc532fb7bed1745bbd418c0e068e7c7d" and "dc6505a2f2c5123bce3903af715280ac4ce8a2f3" have entirely different histories.
92b203cbbc
...
dc6505a2f2
|
|
@ -1,21 +1,3 @@
|
|||
secubox-toolbox (2.6.18-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Cartographie donut-bubble view + geography rollups (#553).
|
||||
- social.py fetch_graph: nodes gain country_iso / country_flag /
|
||||
continent / tier ; new by_country, by_continent (flags + per-tier
|
||||
breakdown) and by_tier aggregations (additive, read-time). stats
|
||||
gains total_countries / total_continents. IP/ASN intentionally
|
||||
NOT used as a graph dimension. Helpers _flag_emoji / _continent_of
|
||||
/ _tracker_tier (no GeoIP/publicsuffix dep).
|
||||
- social.js: new "🍩 Donuts" view (default, ⇄ "👁️ Œil" toggle) —
|
||||
a d3.pack of continent backdrop bubbles, each country a donut sized
|
||||
by tracking impact (hits), ring split by severity tier, flag in the
|
||||
hole ; tier legend ; click → country summary in the detail panel.
|
||||
Eye force-graph kept as one-click fallback ; node detail now fills
|
||||
the country flag.
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Sat, 13 Jun 2026 14:30:00 +0200
|
||||
|
||||
secubox-toolbox (2.6.17-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Social correlation: domain-rollup + history + target↔tracker (#549).
|
||||
|
|
|
|||
|
|
@ -708,58 +708,6 @@ def _registrable_domain(host: str) -> str:
|
|||
return last_two
|
||||
|
||||
|
||||
# ── geography helpers (#553) : flag + continent rollup, no IP/ASN ──
|
||||
def _flag_emoji(iso2: Optional[str]) -> str:
|
||||
"""ISO-3166 alpha-2 → flag emoji (regional indicator pair). '' if unknown."""
|
||||
if not iso2 or len(iso2) != 2 or not iso2.isalpha():
|
||||
return ""
|
||||
base = 0x1F1E6
|
||||
return "".join(chr(base + (ord(ch) - ord("A"))) for ch in iso2.upper())
|
||||
|
||||
|
||||
# Compact ISO-3166 alpha-2 → continent map (covers the countries a French
|
||||
# civic kiosk realistically surfaces ; unknowns fall back to "Autre").
|
||||
_CONTINENT = {
|
||||
# Europe
|
||||
"FR": "Europe", "DE": "Europe", "GB": "Europe", "UK": "Europe", "IE": "Europe",
|
||||
"NL": "Europe", "BE": "Europe", "LU": "Europe", "ES": "Europe", "PT": "Europe",
|
||||
"IT": "Europe", "CH": "Europe", "AT": "Europe", "SE": "Europe", "NO": "Europe",
|
||||
"FI": "Europe", "DK": "Europe", "PL": "Europe", "CZ": "Europe", "RO": "Europe",
|
||||
"BG": "Europe", "GR": "Europe", "HU": "Europe", "SK": "Europe", "HR": "Europe",
|
||||
"RS": "Europe", "UA": "Europe", "RU": "Europe", "IS": "Europe", "EE": "Europe",
|
||||
"LV": "Europe", "LT": "Europe", "SI": "Europe",
|
||||
# Americas
|
||||
"US": "Amériques", "CA": "Amériques", "MX": "Amériques", "BR": "Amériques",
|
||||
"AR": "Amériques", "CL": "Amériques", "CO": "Amériques", "PE": "Amériques",
|
||||
# Asia
|
||||
"CN": "Asie", "JP": "Asie", "KR": "Asie", "IN": "Asie", "SG": "Asie",
|
||||
"HK": "Asie", "TW": "Asie", "TH": "Asie", "ID": "Asie", "VN": "Asie",
|
||||
"IL": "Asie", "TR": "Asie", "AE": "Asie", "SA": "Asie",
|
||||
# Africa
|
||||
"ZA": "Afrique", "EG": "Afrique", "MA": "Afrique", "DZ": "Afrique",
|
||||
"TN": "Afrique", "NG": "Afrique", "KE": "Afrique", "SN": "Afrique",
|
||||
# Oceania
|
||||
"AU": "Océanie", "NZ": "Océanie",
|
||||
}
|
||||
|
||||
|
||||
def _continent_of(iso2: Optional[str]) -> str:
|
||||
if not iso2:
|
||||
return "Inconnu"
|
||||
return _CONTINENT.get(iso2.upper(), "Autre")
|
||||
|
||||
|
||||
def _tracker_tier(opgrade, antibot, cdn) -> str:
|
||||
"""Severity tier used for the donut breakdown (highest wins)."""
|
||||
if opgrade:
|
||||
return "opgrade"
|
||||
if antibot:
|
||||
return "antibot"
|
||||
if cdn:
|
||||
return "cdn"
|
||||
return "other"
|
||||
|
||||
|
||||
def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
||||
"""Return the per-client graph JSON contract.
|
||||
|
||||
|
|
@ -779,8 +727,8 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
|||
# can colour/label by edge-network vendor.
|
||||
for r in c.execute(
|
||||
"SELECT n.tracker_domain, n.hits, n.first_seen, n.last_seen, "
|
||||
"n.sites_jsonl, n.country_iso, m.cdn_vendor, m.cache_status, "
|
||||
"m.antibot_vendor, m.opgrade_vendor, m.opgrade_category "
|
||||
"n.sites_jsonl, m.cdn_vendor, m.cache_status, m.antibot_vendor, "
|
||||
"m.opgrade_vendor, m.opgrade_category "
|
||||
"FROM social_nodes n "
|
||||
"LEFT JOIN social_host_meta m ON m.tracker_domain = n.tracker_domain "
|
||||
"WHERE n.client_mac_hash = ? AND n.last_seen >= ? "
|
||||
|
|
@ -791,7 +739,6 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
|||
sites = json.loads(r["sites_jsonl"])
|
||||
except Exception:
|
||||
sites = []
|
||||
cc = (r["country_iso"] or "").upper() or None
|
||||
out["nodes"].append(
|
||||
{
|
||||
"id": r["tracker_domain"],
|
||||
|
|
@ -801,11 +748,6 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
|||
"sites": sites,
|
||||
"first_seen": r["first_seen"],
|
||||
"last_seen": r["last_seen"],
|
||||
"country_iso": cc,
|
||||
"country_flag": _flag_emoji(cc),
|
||||
"continent": _continent_of(cc),
|
||||
"tier": _tracker_tier(r["opgrade_vendor"], r["antibot_vendor"],
|
||||
r["cdn_vendor"]),
|
||||
"cdn_vendor": r["cdn_vendor"],
|
||||
"cache_status": r["cache_status"],
|
||||
"antibot_vendor": r["antibot_vendor"],
|
||||
|
|
@ -929,52 +871,10 @@ def fetch_graph(mac_hash: str, since_seconds: int = 86400) -> Dict:
|
|||
})
|
||||
out["history"] = history
|
||||
|
||||
# (d) #553 geography + tier rollups for the donut-bubble view.
|
||||
_TIERS = ("opgrade", "antibot", "cdn", "other")
|
||||
tier_tot = {t: 0 for t in _TIERS}
|
||||
_ctry: Dict[str, dict] = {}
|
||||
_cont: Dict[str, dict] = {}
|
||||
for n in out["nodes"]:
|
||||
tier = n["tier"]; hits = n["hits"] or 0
|
||||
tier_tot[tier] += hits
|
||||
cc = n["country_iso"] or "??"
|
||||
ct = _ctry.setdefault(cc, {
|
||||
"country_iso": None if cc == "??" else cc,
|
||||
"flag": n["country_flag"], "continent": n["continent"],
|
||||
"tracker_count": 0, "hits": 0,
|
||||
"tiers": {t: 0 for t in _TIERS}, "_sites": set(),
|
||||
})
|
||||
ct["tracker_count"] += 1; ct["hits"] += hits
|
||||
ct["tiers"][tier] += hits; ct["_sites"].update(n["sites"])
|
||||
co = _cont.setdefault(n["continent"], {
|
||||
"continent": n["continent"], "country_count_set": set(),
|
||||
"tracker_count": 0, "hits": 0, "tiers": {t: 0 for t in _TIERS},
|
||||
})
|
||||
co["country_count_set"].add(cc)
|
||||
co["tracker_count"] += 1; co["hits"] += hits
|
||||
co["tiers"][tier] += hits
|
||||
by_country = sorted(
|
||||
({"country_iso": v["country_iso"], "flag": v["flag"],
|
||||
"continent": v["continent"], "tracker_count": v["tracker_count"],
|
||||
"hits": v["hits"], "sites_count": len(v["_sites"]),
|
||||
"tiers": v["tiers"]} for v in _ctry.values()),
|
||||
key=lambda x: -x["hits"])
|
||||
by_continent = sorted(
|
||||
({"continent": v["continent"],
|
||||
"country_count": len(v["country_count_set"]),
|
||||
"tracker_count": v["tracker_count"], "hits": v["hits"],
|
||||
"tiers": v["tiers"]} for v in _cont.values()),
|
||||
key=lambda x: -x["hits"])
|
||||
out["by_country"] = by_country
|
||||
out["by_continent"] = by_continent
|
||||
out["by_tier"] = tier_tot
|
||||
|
||||
out["stats"] = {
|
||||
"total_trackers": (stats_row["total_trackers"] or 0) if stats_row else 0,
|
||||
"total_sites": sites_count,
|
||||
"total_domains": len(by_domain),
|
||||
"total_countries": len([c for c in by_country if c["country_iso"]]),
|
||||
"total_continents": len([c for c in by_continent if c["continent"] not in ("Inconnu",)]),
|
||||
"first_seen": stats_row["first_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}),
|
||||
|
|
|
|||
|
|
@ -70,54 +70,6 @@
|
|||
|
||||
// ─── graph state ───
|
||||
let simulation = null;
|
||||
let lastGraph = null;
|
||||
let view = 'donuts'; // #553 : 'donuts' (default) | 'eye'
|
||||
|
||||
// Severity-tier palette shared by the donut view + legend.
|
||||
const TIER = {
|
||||
opgrade: { c: 'var(--void-purple)', label: '📡 opérateur' },
|
||||
antibot: { c: 'var(--cinnabar)', label: '🤖 anti-bot' },
|
||||
cdn: { c: 'var(--cyber-cyan)', label: '☁ CDN' },
|
||||
other: { c: 'var(--gold-hermetic)', label: '• autre' },
|
||||
};
|
||||
const TIER_ORDER = ['opgrade', 'antibot', 'cdn', 'other'];
|
||||
|
||||
function draw(graph) {
|
||||
if (!graph) return;
|
||||
if (view === 'donuts') renderDonuts(graph);
|
||||
else render(graph);
|
||||
}
|
||||
|
||||
// Inject the view toggle (🍩 donuts ⇄ 👁️ œil) once, above the svg.
|
||||
function ensureToggle() {
|
||||
if (document.getElementById('view-toggle') || !svgEl) return;
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'view-toggle';
|
||||
bar.style.cssText = 'display:flex;gap:.4rem;justify-content:center;margin:.4rem 0';
|
||||
const mk = (id, txt) => {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button'; b.dataset.view = id; b.textContent = txt;
|
||||
b.style.cssText = 'cursor:pointer;border:1px solid var(--void-purple,#6e40c9);'
|
||||
+ 'background:transparent;color:var(--text-primary,#e8e6d9);border-radius:6px;'
|
||||
+ 'padding:.35rem .8rem;font:inherit';
|
||||
b.addEventListener('click', () => {
|
||||
view = id; syncToggle();
|
||||
draw(lastGraph);
|
||||
});
|
||||
return b;
|
||||
};
|
||||
bar.appendChild(mk('donuts', '🍩 Donuts'));
|
||||
bar.appendChild(mk('eye', '👁️ Œil'));
|
||||
svgEl.parentNode.insertBefore(bar, svgEl);
|
||||
syncToggle();
|
||||
}
|
||||
function syncToggle() {
|
||||
document.querySelectorAll('#view-toggle button').forEach((b) => {
|
||||
const on = b.dataset.view === view;
|
||||
b.style.background = on ? 'var(--void-purple,#6e40c9)' : 'transparent';
|
||||
b.style.color = on ? '#0a0a0f' : 'var(--text-primary,#e8e6d9)';
|
||||
});
|
||||
}
|
||||
|
||||
function svgSize() {
|
||||
// Measure actual rendered size so the force center scales with the
|
||||
|
|
@ -178,8 +130,6 @@
|
|||
sites: n.sites,
|
||||
first_seen: n.first_seen,
|
||||
last_seen: n.last_seen,
|
||||
country_iso: n.country_iso || null,
|
||||
country_flag: n.country_flag || '',
|
||||
cdn_vendor: n.cdn_vendor || null,
|
||||
cache_status: n.cache_status || null,
|
||||
antibot_vendor: n.antibot_vendor || null,
|
||||
|
|
@ -389,118 +339,11 @@
|
|||
});
|
||||
}
|
||||
|
||||
// ─── #553 donut-bubble view ───
|
||||
// Continents are faint backdrop bubbles ; each country is a donut sized
|
||||
// by tracking impact (hits), its ring split by severity tier, with the
|
||||
// flag in the centre. No IP/ASN — geography + impact + tier only.
|
||||
function renderDonuts(graph) {
|
||||
clearGraph();
|
||||
const { W, H } = svgSize();
|
||||
svg.attr('viewBox', `0 0 ${W} ${H}`);
|
||||
bind('total_trackers', graph.stats.total_trackers || 0);
|
||||
bind('total_sites', graph.stats.total_sites || 0);
|
||||
updateAntibotTile(graph.stats.antibot_sites || 0, graph.stats.antibot_vendors || []);
|
||||
updateOpgradeTile(graph.stats.opgrade_sites || 0, graph.stats.opgrade_vendors || []);
|
||||
|
||||
const countries = graph.by_country || [];
|
||||
if (!countries.length) return;
|
||||
|
||||
// hierarchy : root → continent → country(leaf, value=hits)
|
||||
const byCont = new Map();
|
||||
for (const c of countries) {
|
||||
const k = c.continent || 'Autre';
|
||||
if (!byCont.has(k)) byCont.set(k, []);
|
||||
byCont.get(k).push(c);
|
||||
}
|
||||
const root = {
|
||||
name: 'root',
|
||||
children: [...byCont.entries()].map(([cont, list]) => ({
|
||||
name: cont, continent: true,
|
||||
children: list.map(c => ({
|
||||
leaf: true, name: c.country_iso || '??', flag: c.flag || '🏴',
|
||||
value: Math.max(c.hits || 0, 1), tiers: c.tiers || {},
|
||||
tracker_count: c.tracker_count || 0,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
const h = d3.hierarchy(root).sum(d => d.value || 0)
|
||||
.sort((a, b) => (b.value || 0) - (a.value || 0));
|
||||
d3.pack().size([W, H]).padding(d => d.depth === 1 ? 14 : 4)(h);
|
||||
|
||||
const content = svg.append('g').attr('class', 'content');
|
||||
|
||||
// continent backdrop bubbles + labels
|
||||
content.append('g').selectAll('circle.cont')
|
||||
.data(h.descendants().filter(d => d.depth === 1)).join('circle')
|
||||
.attr('class', 'cont')
|
||||
.attr('cx', d => d.x).attr('cy', d => d.y).attr('r', d => d.r)
|
||||
.attr('fill', 'rgba(110,64,201,0.06)')
|
||||
.attr('stroke', 'rgba(110,64,201,0.45)').attr('stroke-dasharray', '3,3');
|
||||
content.append('g').selectAll('text.cont')
|
||||
.data(h.descendants().filter(d => d.depth === 1)).join('text')
|
||||
.attr('class', 'cont').attr('x', d => d.x).attr('y', d => d.y - d.r + 14)
|
||||
.attr('text-anchor', 'middle').attr('fill', 'var(--void-purple,#9e76ff)')
|
||||
.attr('font-size', 12).attr('font-weight', 'bold')
|
||||
.text(d => '🌍 ' + d.data.name);
|
||||
|
||||
// country donut bubbles
|
||||
const pie = d3.pie().sort(null).value(d => d[1]);
|
||||
const leaves = content.append('g').selectAll('g.country')
|
||||
.data(h.leaves()).join('g')
|
||||
.attr('class', 'country node')
|
||||
.attr('transform', d => `translate(${d.x},${d.y})`)
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', (ev, d) => focusCountry(d.data));
|
||||
leaves.each(function (d) {
|
||||
const g = d3.select(this);
|
||||
const r = d.r;
|
||||
const inner = Math.max(r * 0.5, r - 7);
|
||||
const arc = d3.arc().innerRadius(inner).outerRadius(r);
|
||||
const data = TIER_ORDER.map(k => [k, (d.data.tiers && d.data.tiers[k]) || 0])
|
||||
.filter(e => e[1] > 0);
|
||||
const slices = data.length ? pie(data) : pie([['other', 1]]);
|
||||
g.selectAll('path').data(slices).join('path')
|
||||
.attr('d', arc)
|
||||
.attr('fill', s => (TIER[s.data[0]] || TIER.other).c)
|
||||
.attr('stroke', '#0a0a0f').attr('stroke-width', 0.6);
|
||||
// flag (or ISO) in the hole
|
||||
g.append('text').attr('text-anchor', 'middle').attr('dy', '.35em')
|
||||
.attr('font-size', Math.max(9, Math.min(inner * 1.1, 26)))
|
||||
.text(d.data.flag || d.data.name);
|
||||
});
|
||||
|
||||
// tier legend (bottom-left)
|
||||
const lg = content.append('g').attr('transform', `translate(12,${H - 12 - TIER_ORDER.length * 16})`);
|
||||
TIER_ORDER.forEach((k, i) => {
|
||||
const row = lg.append('g').attr('transform', `translate(0,${i * 16})`);
|
||||
row.append('rect').attr('width', 10).attr('height', 10).attr('rx', 2)
|
||||
.attr('fill', TIER[k].c);
|
||||
row.append('text').attr('x', 15).attr('y', 9).attr('font-size', 10)
|
||||
.attr('fill', 'var(--text-muted,#6b6b7a)').text(TIER[k].label);
|
||||
});
|
||||
}
|
||||
|
||||
// Donut country click → reuse the detail panel with a country summary.
|
||||
function focusCountry(c) {
|
||||
if (!ndEl) return;
|
||||
bind('nd_domain', (c.flag || '') + ' ' + (c.name || '?'));
|
||||
bind('nd_country', (c.flag || '') + ' ' + (c.name || '—'));
|
||||
bind('nd_asn', '—');
|
||||
const tier = TIER_ORDER.filter(k => (c.tiers || {})[k])
|
||||
.map(k => `${TIER[k].label}:${c.tiers[k]}`).join(' · ') || '—';
|
||||
bind('nd_cdn', tier);
|
||||
bind('nd_antibot', (c.tiers && c.tiers.antibot) ? '🤖 ' + c.tiers.antibot : '—');
|
||||
bind('nd_opgrade', (c.tiers && c.tiers.opgrade) ? '📡 ' + c.tiers.opgrade : '—');
|
||||
bind('nd_sites', (c.tracker_count || 0) + ' traceurs');
|
||||
bind('nd_first_seen', '—'); bind('nd_last_seen', '—');
|
||||
ndEl.hidden = false;
|
||||
}
|
||||
|
||||
// ─── focus / detail panel ───
|
||||
function focusNode(node, linkSel) {
|
||||
if (node.kind !== 'tracker') { ndEl.hidden = true; return; }
|
||||
bind('nd_domain', node.label);
|
||||
bind('nd_country', node.country_flag ? (node.country_flag + ' ' + (node.country_iso || '')) : '—');
|
||||
bind('nd_country', '—'); // Phase C dependency (GeoIP)
|
||||
bind('nd_asn', '—');
|
||||
bind('nd_cdn', node.cdn_vendor ? (node.cdn_vendor + (node.cache_status ? ' · ' + node.cache_status : '')) : '—');
|
||||
bind('nd_antibot', node.antibot_vendor ? ('🤖 ' + node.antibot_vendor) : '—');
|
||||
|
|
@ -570,9 +413,7 @@
|
|||
const r = await fetch(`/social/graph/${encodeURIComponent(token)}?since=86400`);
|
||||
if (!r.ok) throw new Error('http ' + r.status);
|
||||
const g = await r.json();
|
||||
lastGraph = g;
|
||||
ensureToggle();
|
||||
draw(g);
|
||||
render(g);
|
||||
} catch (e) {
|
||||
console.error('[social] fetch failed', e);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user