mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 12: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.get("device_type", ""),
|
||||
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_country"] = _graph.get("by_country") or []
|
||||
pdf_bytes = reports.render_pdf(data)
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ def render_pdf(report: dict) -> bytes:
|
|||
else:
|
||||
_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
|
||||
_section(pdf, "🔑 IDENTIFIANT ANONYME")
|
||||
_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:
|
||||
if not items:
|
||||
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_text_color(0)
|
||||
pdf.cell(0, 5, _ascii_safe(title), ln=True)
|
||||
for it in items:
|
||||
_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)
|
||||
|
||||
|
||||
# #703 — visual donut charts in the PDF (fpdf2 solid_arc sectors + white hole).
|
||||
# RGB mirror of the HTML report palette.
|
||||
# #703/#711/#714 — charts are rendered with matplotlib to PNG and EMBEDDED as
|
||||
# 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 = [
|
||||
(0, 221, 68), (158, 118, 255), (255, 136, 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:
|
||||
"""Draw one donut (pie sectors + white centre hole) + legend inside a cell of
|
||||
width w at (x, y). segs carry cumulative start/end percents (from _dpi_donut)."""
|
||||
"""Title + embedded donut PNG (left) + text legend (right) in a cell of width w."""
|
||||
fam = getattr(pdf, "_secubox_family", "Helvetica")
|
||||
pdf.set_xy(x, y)
|
||||
pdf.set_font(fam, "B", 9)
|
||||
pdf.set_text_color(0, 90, 64)
|
||||
pdf.cell(w, 5, _ascii_safe(title)[:30], ln=False)
|
||||
cx, cy, r, rh = x + 15, y + 23, 12.5, 8.0
|
||||
if segs:
|
||||
for i, s in enumerate(segs):
|
||||
pdf.set_fill_color(*_PDF_DONUT_PALETTE[i % len(_PDF_DONUT_PALETTE)])
|
||||
a0 = 90.0 - float(s.get("start", 0)) * 3.6
|
||||
a1 = 90.0 - float(s.get("end", 0)) * 3.6
|
||||
png = _mpl_donut_png(segs, hole) if segs else None
|
||||
if png is not None:
|
||||
try:
|
||||
pdf.solid_arc(cx, cy, r, a0, a1, clockwise=True, style="F")
|
||||
pdf.image(png, x=x, y=y + 6, w=28, h=28)
|
||||
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
|
||||
for i, s in enumerate(segs[:6]):
|
||||
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.set_xy(x + 37, ly)
|
||||
pdf.rect(x + 31, ly + 0.6, 2.4, 2.4, style="F")
|
||||
pdf.set_xy(x + 35, ly)
|
||||
pdf.set_font(fam, "", 7)
|
||||
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)
|
||||
ly += 4
|
||||
else:
|
||||
|
|
@ -798,43 +823,104 @@ def _pdf_donut_grid(pdf, donuts: list) -> None:
|
|||
pdf.set_y(y0 + rows * row_h + 2)
|
||||
|
||||
|
||||
# #709 — radial "carto" network map (TOI hub → top trackers) for the PDF.
|
||||
def _carto_graph(pdf, family: str, nodes: list) -> None:
|
||||
def _bars(pdf, family: str, title: str, rows: list, col: tuple = (0, 221, 68)) -> None:
|
||||
"""Horizontal percent bars (label · bar · count). rows = [(label, pct, count)]."""
|
||||
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")],
|
||||
key=lambda n: n.get("hits", 0), reverse=True)[:8]
|
||||
if not nodes:
|
||||
return
|
||||
_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
|
||||
return None
|
||||
maxh = max(n.get("hits", 1) for n in nodes) or 1
|
||||
pdf.set_draw_color(70, 90, 120)
|
||||
pdf.set_line_width(0.2)
|
||||
placed = []
|
||||
fig, ax = plt.subplots(figsize=(6.4, 3.4), dpi=130)
|
||||
for i, n in enumerate(nodes):
|
||||
ang = math.radians(-90 + i * 360.0 / len(nodes))
|
||||
x, y = cx + R * math.cos(ang), cy + R * math.sin(ang)
|
||||
pdf.line(cx, cy, x, y)
|
||||
placed.append((x, y, n))
|
||||
for (x, y, n) in placed:
|
||||
r = 1.6 + 3.2 * (n.get("hits", 1) / maxh)
|
||||
pdf.set_fill_color(255, 80, 110)
|
||||
pdf.ellipse(x - r, y - r, 2 * r, 2 * r, style="F")
|
||||
lbl = f"{n.get('country_flag','')} {(n.get('domain','') or '?')[:12]}"
|
||||
pdf.set_font(family, "", 6)
|
||||
pdf.set_text_color(90, 90, 90)
|
||||
pdf.set_xy(x - 17, (y + r + 0.5) if y >= cy else (y - r - 3.5))
|
||||
pdf.cell(34, 3, _safe(lbl), align="C")
|
||||
pdf.set_fill_color(0, 212, 255)
|
||||
pdf.ellipse(cx - 4.5, cy - 4.5, 9, 9, style="F")
|
||||
pdf.set_xy(cx - 9, cy - 1.6)
|
||||
pdf.set_font(family, "B", 6)
|
||||
pdf.set_text_color(10, 10, 15)
|
||||
pdf.cell(18, 3, "TOI", align="C")
|
||||
pdf.set_text_color(0)
|
||||
pdf.set_y(cy + R + 7)
|
||||
x, y = math.cos(ang), math.sin(ang)
|
||||
ax.plot([0, x], [0, y], color="#3a4a66", lw=0.8, zorder=1)
|
||||
ax.scatter([x], [y], s=120 + 520 * (n.get("hits", 1) / maxh),
|
||||
color="#ff506e", edgecolors="white", linewidths=0.8, zorder=2)
|
||||
iso = (n.get("country_iso") or "").upper()
|
||||
lbl = f"{iso} {(n.get('domain','') or '?')[:16]}".strip()
|
||||
ax.text(x * 1.32, y * 1.32, lbl, fontsize=7, ha="center", va="center", color="#333")
|
||||
ax.scatter([0], [0], s=900, color="#00d4ff", edgecolors="white", linewidths=1.2, zorder=3)
|
||||
ax.text(0, 0, "TOI", fontsize=8, ha="center", va="center", weight="bold", color="#06202a", zorder=4)
|
||||
ax.set_xlim(-2.1, 2.1)
|
||||
ax.set_ylim(-1.7, 1.7)
|
||||
ax.axis("off")
|
||||
buf = BytesIO()
|
||||
fig.savefig(buf, format="png", transparent=True, bbox_inches="tight", pad_inches=0.05)
|
||||
plt.close(fig)
|
||||
buf.seek(0)
|
||||
return buf
|
||||
|
||||
|
||||
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), ...]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user