mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 16:31:31 +00:00
Compare commits
4 Commits
b5764cb52c
...
6aef15bdf7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aef15bdf7 | ||
| ddfe6a7a74 | |||
|
|
2029611010 | ||
| a2e342cfd2 |
|
|
@ -2734,7 +2734,10 @@ async def report_me(request: Request) -> Response:
|
||||||
data["persona"] = _persona_sheet(mac_hash, _lvl, _gs, _exp, data["dpi_exfil"],
|
data["persona"] = _persona_sheet(mac_hash, _lvl, _gs, _exp, data["dpi_exfil"],
|
||||||
data.get("device_type", ""),
|
data.get("device_type", ""),
|
||||||
request.headers.get("user-agent", ""))
|
request.headers.get("user-agent", ""))
|
||||||
data["bestiary"] = (_build_report_charts(_graph).get("trackers") or [])[:5]
|
_charts = _build_report_charts(_graph)
|
||||||
|
data["charts"] = _charts # #711 "En un coup d'œil"
|
||||||
|
data["graph_stats"] = _gs
|
||||||
|
data["bestiary"] = (_charts.get("trackers") or [])[:5]
|
||||||
data["carto_nodes"] = _graph.get("nodes") or [] # #709 carto + tables
|
data["carto_nodes"] = _graph.get("nodes") or [] # #709 carto + tables
|
||||||
data["carto_country"] = _graph.get("by_country") or []
|
data["carto_country"] = _graph.get("by_country") or []
|
||||||
pdf_bytes = reports.render_pdf(data)
|
pdf_bytes = reports.render_pdf(data)
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,10 @@ def render_pdf(report: dict) -> bytes:
|
||||||
else:
|
else:
|
||||||
_dashboard_hero(pdf, family, report)
|
_dashboard_hero(pdf, family, report)
|
||||||
|
|
||||||
|
# ── #711 "En un coup d'œil" : trackers ring + countries/sites bars ──
|
||||||
|
_glance_section(pdf, family, report.get("charts") or {},
|
||||||
|
int((report.get("graph_stats") or {}).get("total_trackers", 0) or 0))
|
||||||
|
|
||||||
# Anonymous ID
|
# Anonymous ID
|
||||||
_section(pdf, "🔑 IDENTIFIANT ANONYME")
|
_section(pdf, "🔑 IDENTIFIANT ANONYME")
|
||||||
_kv(pdf, "Hash session", report.get("mac_hash", "?"))
|
_kv(pdf, "Hash session", report.get("mac_hash", "?"))
|
||||||
|
|
@ -258,7 +262,9 @@ def render_pdf(report: dict) -> bytes:
|
||||||
def _donut_lines(title: str, items: list) -> None:
|
def _donut_lines(title: str, items: list) -> None:
|
||||||
if not items:
|
if not items:
|
||||||
return
|
return
|
||||||
|
pdf.set_x(pdf.l_margin) # #714 reset X so the title isn't clipped right
|
||||||
pdf.set_font(getattr(pdf, "_secubox_family", "Helvetica"), "B", 9)
|
pdf.set_font(getattr(pdf, "_secubox_family", "Helvetica"), "B", 9)
|
||||||
|
pdf.set_text_color(0)
|
||||||
pdf.cell(0, 5, _ascii_safe(title), ln=True)
|
pdf.cell(0, 5, _ascii_safe(title), ln=True)
|
||||||
for it in items:
|
for it in items:
|
||||||
_bullet(pdf, f"{it.get('emoji', '')} {it.get('label', '?')} - {it.get('pct', 0)}%", font_size=8)
|
_bullet(pdf, f"{it.get('emoji', '')} {it.get('label', '?')} - {it.get('pct', 0)}%", font_size=8)
|
||||||
|
|
@ -728,49 +734,68 @@ def _bullet(pdf, text: str, font_size: int = 9) -> None:
|
||||||
pdf.multi_cell(_page_w(pdf), 5, " - " + safe)
|
pdf.multi_cell(_page_w(pdf), 5, " - " + safe)
|
||||||
|
|
||||||
|
|
||||||
# #703 — visual donut charts in the PDF (fpdf2 solid_arc sectors + white hole).
|
# #703/#711/#714 — charts are rendered with matplotlib to PNG and EMBEDDED as
|
||||||
# RGB mirror of the HTML report palette.
|
# raster images (pdf.image), because fpdf2 vector arc/ellipse donuts render in
|
||||||
|
# poppler but blank in iOS/Chrome PDF viewers. Raster PNG displays everywhere.
|
||||||
_PDF_DONUT_PALETTE = [
|
_PDF_DONUT_PALETTE = [
|
||||||
(0, 221, 68), (158, 118, 255), (255, 136, 102),
|
(0, 221, 68), (158, 118, 255), (255, 136, 102),
|
||||||
(102, 187, 255), (255, 179, 71), (255, 68, 102),
|
(102, 187, 255), (255, 179, 71), (255, 68, 102),
|
||||||
]
|
]
|
||||||
|
_PDF_HEX = ["#%02x%02x%02x" % c for c in _PDF_DONUT_PALETTE]
|
||||||
|
|
||||||
|
|
||||||
|
def _mpl_donut_png(segs: list, hole: str = ""):
|
||||||
|
"""Render a donut ring (matplotlib) to a PNG BytesIO. None if no data/mpl."""
|
||||||
|
try:
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from io import BytesIO
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
vals, cols = [], []
|
||||||
|
for i, s in enumerate(segs or []):
|
||||||
|
v = s.get("pct") or s.get("count") or 0
|
||||||
|
if v:
|
||||||
|
vals.append(v)
|
||||||
|
cols.append(_PDF_HEX[i % len(_PDF_HEX)])
|
||||||
|
if not vals:
|
||||||
|
return None
|
||||||
|
fig, ax = plt.subplots(figsize=(1.7, 1.7), dpi=130)
|
||||||
|
ax.pie(vals, colors=cols, startangle=90, counterclock=False,
|
||||||
|
wedgeprops=dict(width=0.42, edgecolor="white", linewidth=1.2))
|
||||||
|
if hole:
|
||||||
|
ax.text(0, 0, str(hole)[:8], ha="center", va="center",
|
||||||
|
fontsize=9, color="#444", weight="bold")
|
||||||
|
ax.set(aspect="equal")
|
||||||
|
buf = BytesIO()
|
||||||
|
fig.savefig(buf, format="png", transparent=True, bbox_inches="tight", pad_inches=0.02)
|
||||||
|
plt.close(fig)
|
||||||
|
buf.seek(0)
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
def _pdf_donut(pdf, x: float, y: float, w: float, title: str, hole: str, segs: list) -> None:
|
def _pdf_donut(pdf, x: float, y: float, w: float, title: str, hole: str, segs: list) -> None:
|
||||||
"""Draw one donut (pie sectors + white centre hole) + legend inside a cell of
|
"""Title + embedded donut PNG (left) + text legend (right) in a cell of width w."""
|
||||||
width w at (x, y). segs carry cumulative start/end percents (from _dpi_donut)."""
|
|
||||||
fam = getattr(pdf, "_secubox_family", "Helvetica")
|
fam = getattr(pdf, "_secubox_family", "Helvetica")
|
||||||
pdf.set_xy(x, y)
|
pdf.set_xy(x, y)
|
||||||
pdf.set_font(fam, "B", 9)
|
pdf.set_font(fam, "B", 9)
|
||||||
pdf.set_text_color(0, 90, 64)
|
pdf.set_text_color(0, 90, 64)
|
||||||
pdf.cell(w, 5, _ascii_safe(title)[:30], ln=False)
|
pdf.cell(w, 5, _ascii_safe(title)[:30], ln=False)
|
||||||
cx, cy, r, rh = x + 15, y + 23, 12.5, 8.0
|
png = _mpl_donut_png(segs, hole) if segs else None
|
||||||
if segs:
|
if png is not None:
|
||||||
for i, s in enumerate(segs):
|
try:
|
||||||
pdf.set_fill_color(*_PDF_DONUT_PALETTE[i % len(_PDF_DONUT_PALETTE)])
|
pdf.image(png, x=x, y=y + 6, w=28, h=28)
|
||||||
a0 = 90.0 - float(s.get("start", 0)) * 3.6
|
except Exception:
|
||||||
a1 = 90.0 - float(s.get("end", 0)) * 3.6
|
pass
|
||||||
try:
|
|
||||||
pdf.solid_arc(cx, cy, r, a0, a1, clockwise=True, style="F")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# centre hole (page is white)
|
|
||||||
pdf.set_fill_color(255, 255, 255)
|
|
||||||
pdf.ellipse(cx - rh, cy - rh, 2 * rh, 2 * rh, style="F")
|
|
||||||
if hole:
|
|
||||||
pdf.set_xy(cx - rh, cy - 2)
|
|
||||||
pdf.set_font(fam, "", 6)
|
|
||||||
pdf.set_text_color(110, 110, 110)
|
|
||||||
pdf.cell(2 * rh, 4, _ascii_safe(hole)[:8], align="C")
|
|
||||||
# legend (right of the donut)
|
|
||||||
ly = y + 8
|
ly = y + 8
|
||||||
for i, s in enumerate(segs[:6]):
|
for i, s in enumerate(segs[:6]):
|
||||||
pdf.set_fill_color(*_PDF_DONUT_PALETTE[i % len(_PDF_DONUT_PALETTE)])
|
pdf.set_fill_color(*_PDF_DONUT_PALETTE[i % len(_PDF_DONUT_PALETTE)])
|
||||||
pdf.rect(x + 33, ly + 0.6, 2.4, 2.4, style="F")
|
pdf.rect(x + 31, ly + 0.6, 2.4, 2.4, style="F")
|
||||||
pdf.set_xy(x + 37, ly)
|
pdf.set_xy(x + 35, ly)
|
||||||
pdf.set_font(fam, "", 7)
|
pdf.set_font(fam, "", 7)
|
||||||
pdf.set_text_color(40, 40, 40)
|
pdf.set_text_color(40, 40, 40)
|
||||||
pdf.cell(w - 38, 3.5,
|
pdf.cell(w - 36, 3.5,
|
||||||
_ascii_safe(f"{s.get('label', '?')[:16]} {s.get('pct', 0)}%"), ln=False)
|
_ascii_safe(f"{s.get('label', '?')[:16]} {s.get('pct', 0)}%"), ln=False)
|
||||||
ly += 4
|
ly += 4
|
||||||
else:
|
else:
|
||||||
|
|
@ -798,43 +823,104 @@ def _pdf_donut_grid(pdf, donuts: list) -> None:
|
||||||
pdf.set_y(y0 + rows * row_h + 2)
|
pdf.set_y(y0 + rows * row_h + 2)
|
||||||
|
|
||||||
|
|
||||||
# #709 — radial "carto" network map (TOI hub → top trackers) for the PDF.
|
def _bars(pdf, family: str, title: str, rows: list, col: tuple = (0, 221, 68)) -> None:
|
||||||
def _carto_graph(pdf, family: str, nodes: list) -> None:
|
"""Horizontal percent bars (label · bar · count). rows = [(label, pct, count)]."""
|
||||||
import math
|
rows = [r for r in (rows or []) if r]
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
pdf.set_x(pdf.l_margin)
|
||||||
|
pdf.set_font(family, "B", 9)
|
||||||
|
pdf.set_text_color(110, 64, 201)
|
||||||
|
pdf.cell(0, 5, _safe(title), ln=True)
|
||||||
|
w = _page_w(pdf)
|
||||||
|
lblw, valw = 40.0, 13.0
|
||||||
|
barw = w - lblw - valw
|
||||||
|
pdf.set_font(family, "", 8)
|
||||||
|
for (lbl, pct, cnt) in rows[:6]:
|
||||||
|
pct = max(0, min(100, int(pct or 0)))
|
||||||
|
pdf.set_x(pdf.l_margin)
|
||||||
|
pdf.set_text_color(40, 40, 40)
|
||||||
|
pdf.cell(lblw, 4.6, _safe(str(lbl))[:22], ln=False)
|
||||||
|
bx, by = pdf.get_x(), pdf.get_y()
|
||||||
|
pdf.set_fill_color(234, 236, 240)
|
||||||
|
pdf.rect(bx, by + 0.9, barw, 2.8, style="F")
|
||||||
|
pdf.set_fill_color(*col)
|
||||||
|
pdf.rect(bx, by + 0.9, barw * pct / 100.0, 2.8, style="F")
|
||||||
|
pdf.set_xy(bx + barw + 1, by)
|
||||||
|
pdf.set_text_color(0, 150, 60)
|
||||||
|
pdf.cell(valw, 4.6, str(cnt), ln=True)
|
||||||
|
pdf.ln(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _glance_section(pdf, family: str, charts: dict, n_trackers: int) -> None:
|
||||||
|
"""#711 — '📊 En un coup d'œil' for the PDF: trackers ring + countries +
|
||||||
|
most-tracked sites bars (same content as the HTML report card)."""
|
||||||
|
ch = charts or {}
|
||||||
|
if not (ch.get("trackers") or ch.get("countries") or ch.get("sites")):
|
||||||
|
return
|
||||||
|
_section(pdf, "📊 EN UN COUP D'ŒIL")
|
||||||
|
y0 = pdf.get_y()
|
||||||
|
_pdf_donut(pdf, pdf.l_margin, y0, _page_w(pdf), "🍪 Qui te trace",
|
||||||
|
str(n_trackers), ch.get("trackers") or [])
|
||||||
|
pdf.set_y(y0 + 38)
|
||||||
|
_bars(pdf, family, "🌍 Vers quels pays",
|
||||||
|
[(f"{c.get('flag', '')} {c.get('label', '?')}", c.get("pct", 0), c.get("count", 0))
|
||||||
|
for c in (ch.get("countries") or [])], col=(0, 221, 68))
|
||||||
|
_bars(pdf, family, "🌐 Où tu es le plus pisté (traceurs/site)",
|
||||||
|
[(s.get("label", "?"), s.get("pct", 0), s.get("count", 0))
|
||||||
|
for s in (ch.get("sites") or [])], col=(158, 118, 255))
|
||||||
|
|
||||||
|
|
||||||
|
# #709/#714 — radial "carto" hub map rendered with matplotlib → embedded PNG.
|
||||||
|
def _mpl_carto_png(nodes: list):
|
||||||
|
try:
|
||||||
|
import math
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from io import BytesIO
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
nodes = sorted([n for n in (nodes or []) if n.get("hits")],
|
nodes = sorted([n for n in (nodes or []) if n.get("hits")],
|
||||||
key=lambda n: n.get("hits", 0), reverse=True)[:8]
|
key=lambda n: n.get("hits", 0), reverse=True)[:8]
|
||||||
if not nodes:
|
if not nodes:
|
||||||
return
|
return None
|
||||||
_section(pdf, "🗺️ CARTO — qui te piste (carte du réseau)")
|
|
||||||
cx = pdf.l_margin + _page_w(pdf) / 2.0
|
|
||||||
cy = pdf.get_y() + 34
|
|
||||||
R = 27.0
|
|
||||||
maxh = max(n.get("hits", 1) for n in nodes) or 1
|
maxh = max(n.get("hits", 1) for n in nodes) or 1
|
||||||
pdf.set_draw_color(70, 90, 120)
|
fig, ax = plt.subplots(figsize=(6.4, 3.4), dpi=130)
|
||||||
pdf.set_line_width(0.2)
|
|
||||||
placed = []
|
|
||||||
for i, n in enumerate(nodes):
|
for i, n in enumerate(nodes):
|
||||||
ang = math.radians(-90 + i * 360.0 / len(nodes))
|
ang = math.radians(-90 + i * 360.0 / len(nodes))
|
||||||
x, y = cx + R * math.cos(ang), cy + R * math.sin(ang)
|
x, y = math.cos(ang), math.sin(ang)
|
||||||
pdf.line(cx, cy, x, y)
|
ax.plot([0, x], [0, y], color="#3a4a66", lw=0.8, zorder=1)
|
||||||
placed.append((x, y, n))
|
ax.scatter([x], [y], s=120 + 520 * (n.get("hits", 1) / maxh),
|
||||||
for (x, y, n) in placed:
|
color="#ff506e", edgecolors="white", linewidths=0.8, zorder=2)
|
||||||
r = 1.6 + 3.2 * (n.get("hits", 1) / maxh)
|
iso = (n.get("country_iso") or "").upper()
|
||||||
pdf.set_fill_color(255, 80, 110)
|
lbl = f"{iso} {(n.get('domain','') or '?')[:16]}".strip()
|
||||||
pdf.ellipse(x - r, y - r, 2 * r, 2 * r, style="F")
|
ax.text(x * 1.32, y * 1.32, lbl, fontsize=7, ha="center", va="center", color="#333")
|
||||||
lbl = f"{n.get('country_flag','')} {(n.get('domain','') or '?')[:12]}"
|
ax.scatter([0], [0], s=900, color="#00d4ff", edgecolors="white", linewidths=1.2, zorder=3)
|
||||||
pdf.set_font(family, "", 6)
|
ax.text(0, 0, "TOI", fontsize=8, ha="center", va="center", weight="bold", color="#06202a", zorder=4)
|
||||||
pdf.set_text_color(90, 90, 90)
|
ax.set_xlim(-2.1, 2.1)
|
||||||
pdf.set_xy(x - 17, (y + r + 0.5) if y >= cy else (y - r - 3.5))
|
ax.set_ylim(-1.7, 1.7)
|
||||||
pdf.cell(34, 3, _safe(lbl), align="C")
|
ax.axis("off")
|
||||||
pdf.set_fill_color(0, 212, 255)
|
buf = BytesIO()
|
||||||
pdf.ellipse(cx - 4.5, cy - 4.5, 9, 9, style="F")
|
fig.savefig(buf, format="png", transparent=True, bbox_inches="tight", pad_inches=0.05)
|
||||||
pdf.set_xy(cx - 9, cy - 1.6)
|
plt.close(fig)
|
||||||
pdf.set_font(family, "B", 6)
|
buf.seek(0)
|
||||||
pdf.set_text_color(10, 10, 15)
|
return buf
|
||||||
pdf.cell(18, 3, "TOI", align="C")
|
|
||||||
pdf.set_text_color(0)
|
|
||||||
pdf.set_y(cy + R + 7)
|
def _carto_graph(pdf, family: str, nodes: list) -> None:
|
||||||
|
png = _mpl_carto_png(nodes)
|
||||||
|
if png is None:
|
||||||
|
return
|
||||||
|
_section(pdf, "🗺️ CARTO — qui te piste (carte du réseau)")
|
||||||
|
w = _page_w(pdf)
|
||||||
|
h = w * 0.5
|
||||||
|
y0 = pdf.get_y()
|
||||||
|
try:
|
||||||
|
pdf.image(png, x=pdf.l_margin, y=y0, w=w, h=h)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
pdf.set_y(y0 + h + 3)
|
||||||
|
|
||||||
|
|
||||||
# #709 — generic emoji data table. cols = [(header, width_fraction), ...]
|
# #709 — generic emoji data table. cols = [(header, width_fraction), ...]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user