Skip to content
API Documentation
BastionGuard™
ConnectionsPage.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™ Secure Connection
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 * GPL-3.0 — BastionGuard™ is a trademark of Calogero Scarnà.
5 */
6
7#include "ConnectionsPage.hpp"
9#include <glibmm/i18n.h>
10#include <glibmm/datetime.h>
11#include <gtkmm/cellrenderercombo.h>
12#include <gtkmm/cellrendererpixbuf.h>
13#include <gtkmm/cellrenderertext.h>
14#include <giomm/appinfo.h>
15#include <giomm/desktopappinfo.h>
16#include <giomm/themedicon.h>
17#include <iostream>
18#include <fstream>
19#include <sstream>
20#include <filesystem>
21#include <cstdlib>
22#include <cstdio>
23#include <ctime>
24#include <cctype>
25#include <algorithm>
26#include <unistd.h>
27
28static const char* kVerdictOptions[] = {
29 "allow once", "allow always", "deny once", "deny always", nullptr
30};
31
32// ---------------------------------------------------------------------------
33// Costruttore
34// ---------------------------------------------------------------------------
35
37: Gtk::Box(Gtk::Orientation::VERTICAL, 0)
38, m_uiServer(uiServer)
39{
45
46 // Stack con due pagine. Default: apps.
47 m_appScroll.set_child(m_appTreeView);
48 m_appScroll.set_vexpand(true);
49 m_scroll.set_child(m_treeView);
50 m_scroll.set_vexpand(true);
51
52 m_stack.add(m_appScroll, "apps", _("Applicazioni"));
53 m_stack.add(m_scroll, "detail", _("Dettagli connessioni"));
54 m_stack.set_transition_type(Gtk::StackTransitionType::SLIDE_LEFT_RIGHT);
55 m_stack.set_transition_duration(220);
56 m_stack.set_vexpand(true);
57 m_stack.set_visible_child("apps");
58
59 append(m_toolbar);
60 append(m_stack);
61 append(m_sparkFrame);
62
63 // Al primo avvio siamo in apps → backBtn nascosto, detailTitle nascosto.
65
66 // Avvia il timer dello sparkline
67 m_sparkTick = Glib::signal_timeout().connect(
68 sigc::mem_fun(*this, &ConnectionsPage::onSparkTick),
70}
71
73{
74 // Disconnetti il timer prima della distruzione: Glib::signal_timeout()
75 // e' un source globale; se la pagina viene distrutta (es. cambio
76 // finestra principale) il callback risulterebbe in use-after-free.
77 m_sparkTick.disconnect();
78}
79
81{
82 m_verdictModel = Gtk::ListStore::create(m_comboCol);
83 for (int i = 0; kVerdictOptions[i]; ++i) {
84 auto row = *m_verdictModel->append();
86 }
87}
88
89// ---------------------------------------------------------------------------
90// Toolbar
91// ---------------------------------------------------------------------------
92
94{
95 m_toolbar.set_margin_top(8);
96 m_toolbar.set_margin_bottom(8);
97 m_toolbar.set_margin_start(8);
98 m_toolbar.set_margin_end(8);
99
100 // Pulsante "Indietro" — visibile solo in vista detail
101 m_backBtn.set_label("←");
102 m_backBtn.set_tooltip_text(_("Torna all'elenco applicazioni"));
103 m_backBtn.signal_clicked().connect(
104 sigc::mem_fun(*this, &ConnectionsPage::onBackClicked));
105 m_toolbar.append(m_backBtn);
106
107 // Titolo centrale (visibile solo in detail)
108 m_detailTitle.set_halign(Gtk::Align::START);
109 m_detailTitle.set_valign(Gtk::Align::CENTER);
110 m_detailTitle.get_style_context()->add_class("heading");
111 m_toolbar.append(m_detailTitle);
112
113 m_search.set_placeholder_text(_("Filtra per app o host..."));
114 m_search.set_hexpand(true);
115 m_toolbar.append(m_search);
116
117 m_clearBtn.set_label(_("🗑 Pulisci"));
118 m_clearBtn.signal_clicked().connect(
119 sigc::mem_fun(*this, &ConnectionsPage::onClear));
120 m_toolbar.append(m_clearBtn);
121}
122
123// ---------------------------------------------------------------------------
124// Vista APP aggregata (stato default "apps")
125// Colonne: Applicazione | Connessioni | Dati stimati | Ultimo accesso
126// ---------------------------------------------------------------------------
127
129{
130 m_appStore = Gtk::ListStore::create(m_appColumns);
131 m_appTreeView.set_model(m_appStore);
132 m_appTreeView.get_style_context()->add_class("bsc-treeview");
133 m_appTreeView.set_grid_lines(Gtk::TreeView::GridLines::HORIZONTAL);
134
135 // ── Colonna "Applicazione": icona + testo ─────────────────────────
136 // Composta da due renderer affiancati: CellRendererPixbuf (icona dal
137 // tema) + CellRendererText (nome processo). Lookup dell'icona via
138 // DesktopAppInfo — vedi resolveIconName().
139 {
140 auto* col = Gtk::make_managed<Gtk::TreeViewColumn>(_("Applicazione"));
141 auto* iconCell = Gtk::make_managed<Gtk::CellRendererPixbuf>();
142 auto* textCell = Gtk::make_managed<Gtk::CellRendererText>();
143
144 // Icona 24×24 — stessa scala usata da Nautilus/Files in lista.
145 iconCell->property_icon_size() = Gtk::IconSize::NORMAL;
146 iconCell->set_padding(2, 0);
147 textCell->set_padding(4, 0);
148
149 col->pack_start(*iconCell, /*expand=*/false);
150 col->pack_start(*textCell, /*expand=*/true);
151 col->add_attribute(iconCell->property_icon_name(), m_appColumns.col_icon_name);
152 col->add_attribute(textCell->property_text(), m_appColumns.col_app);
153 col->set_sort_column(m_appColumns.col_app);
154 m_appTreeView.append_column(*col);
155 }
156
157 m_appTreeView.append_column(_("Connessioni"), m_appColumns.col_conn_count);
158 m_appTreeView.append_column(_("Dati stimati"), m_appColumns.col_bytes_str);
159 m_appTreeView.append_column(_("Ultimo accesso"), m_appColumns.col_last_seen);
160
161 // Larghezze minime
162 const int minW[] = { 260, 110, 130, 130 };
163 for (int i = 0; i < 4; ++i) {
164 if (auto* c = m_appTreeView.get_column(i)) {
165 c->set_min_width(minW[i]);
166 c->set_resizable(true);
167 }
168 }
169
170 // Rendi le colonne ordinabili con lo stesso campo "grezzo" quando serve.
171 // Colonna "Connessioni": ordina direttamente sul uint32.
172 if (auto* c = m_appTreeView.get_column(1)) {
173 c->set_sort_column(m_appColumns.col_conn_count);
174 }
175 // Colonna "Dati stimati": ordina sul valore raw byte, non sulla stringa
176 // formattata (altrimenti "900 KB" > "1.2 MB" perche' confronto lessicale).
177 if (auto* c = m_appTreeView.get_column(2)) {
178 c->set_sort_column(m_appColumns.col_bytes_raw);
179 }
180 // Colonna "Ultimo accesso": ordina su epoch, non su stringa HH:MM:SS.
181 if (auto* c = m_appTreeView.get_column(3)) {
182 c->set_sort_column(m_appColumns.col_last_ts);
183 }
184 // Sort iniziale: piu' recenti in cima.
185 m_appStore->set_sort_column(m_appColumns.col_last_ts,
186 Gtk::SortType::DESCENDING);
187
188 // Doppio click sull'app → entra nella vista detail filtrata.
189 m_appTreeView.signal_row_activated().connect(
190 sigc::mem_fun(*this, &ConnectionsPage::onAppActivated));
191}
192
193// ---------------------------------------------------------------------------
194// Vista DETAIL — lista completa connessioni di UNA app
195// Ordine colonne (v2): Ora | Applicazione | Esito | # | PID | Protocollo |
196// IP Sorgente | Host | IP Dest. | Porta | Argomenti
197// ---------------------------------------------------------------------------
198
200{
201 m_store = Gtk::ListStore::create(m_columns);
202 m_treeView.set_model(m_store);
203 m_treeView.get_style_context()->add_class("bsc-treeview");
204 m_treeView.set_grid_lines(Gtk::TreeView::GridLines::HORIZONTAL);
205
206 m_treeView.append_column(_("Ora"), m_columns.col_time);
207 m_treeView.append_column(_("Applicazione"), m_columns.col_app);
208
209 // Colonna Esito — CellRendererCombo (subito dopo Applicazione)
210 {
211 auto* col = Gtk::make_managed<Gtk::TreeViewColumn>(_("Esito"));
212 auto* cell = Gtk::make_managed<Gtk::CellRendererCombo>();
213 cell->property_model() = m_verdictModel;
214 cell->property_text_column() = 0;
215 cell->property_editable() = true;
216 cell->property_has_entry() = false;
217 cell->signal_edited().connect(
218 sigc::mem_fun(*this, &ConnectionsPage::onVerdictEdited));
219 col->pack_start(*cell, true);
220 col->add_attribute(cell->property_text(), m_columns.col_verdict);
221
222 col->set_cell_data_func(*cell,
223 [this](Gtk::CellRenderer* cr, const Gtk::TreeModel::const_iterator& it) {
224 auto* c = static_cast<Gtk::CellRendererCombo*>(cr);
225 bool pending = (*it)[m_columns.col_pending];
226 c->property_foreground() = pending ? "#f0c040" : "";
227 c->property_weight() = pending
228 ? static_cast<int>(Pango::Weight::BOLD)
229 : static_cast<int>(Pango::Weight::NORMAL);
230 });
231
232 m_treeView.append_column(*col);
233 }
234
235 m_treeView.append_column(_("#"), m_columns.col_count);
236 m_treeView.append_column(_("PID"), m_columns.col_pid);
237 m_treeView.append_column(_("Protocollo"), m_columns.col_protocol);
238 m_treeView.append_column(_("IP Sorgente"), m_columns.col_src_ip);
239 m_treeView.append_column(_("Host"), m_columns.col_host);
240 m_treeView.append_column(_("IP Dest."), m_columns.col_ip);
241 m_treeView.append_column(_("Porta"), m_columns.col_port);
242 m_treeView.append_column(_("Argomenti"), m_columns.col_args);
243
244 const int minW[] = { 75, 180, 140, 40, 55, 75, 110, 160, 110, 55, 180 };
245 for (int i = 0; i < 11; ++i) {
246 if (auto* c = m_treeView.get_column(i)) {
247 c->set_min_width(minW[i]);
248 c->set_resizable(true);
249 }
250 }
251}
252
253// ---------------------------------------------------------------------------
254// Sparkline throughput stimato (60 s)
255// ---------------------------------------------------------------------------
256
258{
259 m_sparkFrame.set_margin_top(4);
260 m_sparkFrame.set_margin_bottom(4);
261 m_sparkFrame.set_margin_start(8);
262 m_sparkFrame.set_margin_end(8);
263
264 m_sparkLabel.set_markup(
265 _("<b>Throughput stimato</b> <small>(ultimi 60 s — stima basata sul numero di connessioni)</small>"));
266 m_sparkLabel.set_halign(Gtk::Align::START);
267 m_sparkLabel.set_margin_start(8);
268 m_sparkLabel.set_margin_top(4);
269
270 m_sparkArea.set_content_height(72);
271 m_sparkArea.set_hexpand(true);
272 m_sparkArea.set_draw_func(
273 sigc::mem_fun(*this, &ConnectionsPage::onSparkDraw));
274
275 m_sparkBox.append(m_sparkLabel);
276 m_sparkBox.append(m_sparkArea);
277 m_sparkFrame.set_child(m_sparkBox);
278
279 // Prealloca 120 bucket a zero: evita saltelli all'avvio della UI.
280 for (int i = 0; i < kSparkBucketsMax; ++i) m_sparkBuckets.push_back(0);
281}
282
283// ---------------------------------------------------------------------------
284// Navigazione: apps ↔ detail
285// ---------------------------------------------------------------------------
286
288{
289 m_stack.set_visible_child("apps");
290 m_backBtn.set_visible(false);
291 m_detailTitle.set_visible(false);
292 m_currentAppFilter.clear();
293 // Svuota il modello detail per liberare memoria: verra' ripopolato
294 // quando l'utente rientra in una specifica app (filtrando le connessioni
295 // arrivate nel frattempo dal log / dagli aggregati).
296 //
297 // NOTA: NON tocchiamo m_appStats / m_appIndex (servono agli aggregati).
298 m_store->clear();
299 m_groupIndex.clear();
300}
301
302void ConnectionsPage::showDetailView(const std::string& app)
303{
304 m_currentAppFilter = app;
305 m_detailTitle.set_text(Glib::ustring::compose(
306 _("Connessioni di %1"), app));
307 m_detailTitle.set_visible(true);
308 m_backBtn.set_visible(true);
309 m_stack.set_visible_child("detail");
310
311 // Popola dalla history persistita per questa app (ring buffer di
312 // 500 eventi max). Cosi' rientrando in un'app che era gia' stata
313 // attiva vedi lo storico invece di una schermata vuota —
314 // continuando ad aggiungere in cima i nuovi eventi live.
315 //
316 // PERF: stacchiamo il modello PRIMA di clear() + append, cosi'
317 // evitiamo sia il burst di row-deleted (se passiamo da detail-X
318 // a detail-Y senza tornare indietro: m_store puo' avere fino a
319 // 500 righe) sia il burst di row-inserted (ring buffer della
320 // nuova app). populateDetailFromRing ha gia' il suo
321 // unset/set_model, ma se lo chiamiamo qui con il modello gia'
322 // staccato, il suo set_model finale si occupa di riattaccare.
323 m_treeView.unset_model();
324 m_store->clear();
325 m_groupIndex.clear();
327 // Safety net: populateDetailFromRing esce subito se il ring e'
328 // vuoto, senza riattaccare il modello. Garantiamo qui che la
329 // view torni sempre associata a m_store.
330 if (m_treeView.get_model() != m_store) {
331 m_treeView.set_model(m_store);
332 }
333}
334
339
340void ConnectionsPage::onAppActivated(const Gtk::TreeModel::Path& path,
341 Gtk::TreeViewColumn* /*column*/)
342{
343 auto it = m_appStore->get_iter(path);
344 if (!it) return;
345 const Glib::ustring app = (*it)[m_appColumns.col_app];
346 showDetailView(app.raw());
347}
348
349// ---------------------------------------------------------------------------
350// Risoluzione icone app — via DesktopAppInfo + fallback
351// ---------------------------------------------------------------------------
352
354{
355 if (m_desktopIndexBuilt) return;
356 m_desktopIndexBuilt = true;
357
358 // Scansione di tutti i .desktop visibili nel sistema. Per ciascuno
359 // estraiamo il nome del binario (executable basename) e lo usiamo
360 // come chiave lowercase. Es: /usr/share/applications/firefox.desktop
361 // con Exec="firefox %u" → mappa "firefox" → "firefox" (icon name).
362 //
363 // Questo copre la stragrande maggioranza delle applicazioni GUI.
364 // I daemon di sistema (dnsmasq, avahi-daemon, irq handlers) non
365 // hanno .desktop, quindi cadono sul fallback generico.
366 auto apps = Gio::AppInfo::get_all();
367 for (const auto& ai : apps) {
368 auto dai = std::dynamic_pointer_cast<Gio::DesktopAppInfo>(ai);
369 if (!dai) continue;
370
371 // Icon name: il nome canonico dell'icona nel tema
372 Glib::ustring icon_name;
373 auto icon = dai->get_icon();
374 if (icon) {
375 auto themed = std::dynamic_pointer_cast<Gio::ThemedIcon>(icon);
376 if (themed) {
377 const auto names = themed->get_names();
378 if (!names.empty()) icon_name = names[0];
379 }
380 }
381 if (icon_name.empty()) continue;
382
383 // Exec line: prendiamo il primo token (path) e il basename.
384 const std::string exec = dai->get_commandline();
385 if (exec.empty()) continue;
386 // Primo token (fino a spazio)
387 auto sp = exec.find(' ');
388 std::string first = (sp == std::string::npos) ? exec : exec.substr(0, sp);
389 // Basename
390 auto sl = first.rfind('/');
391 std::string base = (sl == std::string::npos) ? first : first.substr(sl + 1);
392 // Lowercase per lookup case-insensitive
393 for (auto& c : base) c = std::tolower(static_cast<unsigned char>(c));
394 if (base.empty()) continue;
395
396 // Non sovrascrivere: se esistono piu' .desktop con stesso exec
397 // (es. "firefox" + "firefox-esr" entrambi con Exec=firefox) il
398 // primo vince. Ordine deterministico non garantito ma in
399 // pratica va bene.
400 m_desktopIndex.emplace(base, icon_name.raw());
401
402 // Indicizza anche il nome "StartupWMClass" se presente — alcune
403 // app (Discord, Slack) usano questo come identificatore.
404 const std::string wm = dai->get_startup_wm_class();
405 if (!wm.empty()) {
406 std::string wmlc = wm;
407 for (auto& c : wmlc) c = std::tolower(static_cast<unsigned char>(c));
408 m_desktopIndex.emplace(wmlc, icon_name.raw());
409 }
410 }
411}
412
413std::string ConnectionsPage::resolveIconName(const std::string& app_display)
414{
415 if (app_display == bsc::ui_util::safe_local_process_label()) {
417 }
418 return bsc::ui_util::best_app_icon_name(app_display, app_display);
419}
420
421// ---------------------------------------------------------------------------
422// Chiave di raggruppamento — app + proto + host|ip + port
423// ---------------------------------------------------------------------------
424
425std::string ConnectionsPage::groupKeyFor(const std::string& app,
426 const std::string& protocol,
427 const std::string& host,
428 const std::string& ip,
429 uint32_t port)
430{
431 if (app.empty()) return std::string{};
432 const std::string h = !host.empty() ? host : ip;
433 return app + "|" + protocol + "|" + h + "|" + std::to_string(port);
434}
435
436// ---------------------------------------------------------------------------
437// Formattazione byte e tempo
438// ---------------------------------------------------------------------------
439
440std::string ConnectionsPage::formatBytes(uint64_t n)
441{
442 char buf[32];
443 if (n < 1024ULL) {
444 std::snprintf(buf, sizeof(buf), "%lu B", (unsigned long)n);
445 } else if (n < 1024ULL * 1024ULL) {
446 std::snprintf(buf, sizeof(buf), "%.1f KB", n / 1024.0);
447 } else if (n < 1024ULL * 1024ULL * 1024ULL) {
448 std::snprintf(buf, sizeof(buf), "%.1f MB", n / (1024.0 * 1024.0));
449 } else {
450 std::snprintf(buf, sizeof(buf), "%.2f GB",
451 n / (1024.0 * 1024.0 * 1024.0));
452 }
453 return std::string(buf);
454}
455
456std::string ConnectionsPage::formatEpochTime(int64_t epoch)
457{
458 if (epoch <= 0) return "";
459 std::time_t t = static_cast<std::time_t>(epoch);
460 std::tm tmv{};
461 localtime_r(&t, &tmv);
462 char buf[16];
463 std::strftime(buf, sizeof(buf), "%H:%M:%S", &tmv);
464 return std::string(buf);
465}
466
467// ---------------------------------------------------------------------------
468// onNewConnection: orchestra aggiornamento apps + detail + sparkline
469// ---------------------------------------------------------------------------
470
472{
473 // Nome applicazione risolto da .desktop quando possibile
474 const std::string app_display = bsc::ui_util::best_connection_display_name(
475 info.app_comm, info.app_path, info.app_args, info.dest_ip, info.src_ip);
476
477 // Argomenti come stringa
478 std::string argsStr;
479 for (size_t i = 0; i < info.app_args.size(); ++i) {
480 if (i > 0) argsStr += ' ';
481 argsStr += info.app_args[i];
482 }
483
484 // 1) Sparkline: aggiungi al bucket corrente (gli eventi pending contano
485 // comunque come traffico intenzionale — l'utente potrebbe anche
486 // negarli, ma nel frattempo il socket e' stato aperto).
488
489 // 2) Aggregati per-app (lista principale)
490 if (!app_display.empty()) {
491 updateAppAggregate(app_display);
492 }
493
494 // 3) Ring buffer per-app: persistiamo ogni evento detail (non solo
495 // se siamo in vista detail). Cosi' entrando in un'app al prossimo
496 // avvio (o semplicemente nella stessa sessione) vediamo lo storico
497 // anche se non era mai stata aperta la sua detail view.
498 if (!app_display.empty()) {
499 DetailEvent e;
500 e.ts_epoch = static_cast<int64_t>(std::time(nullptr));
501 e.time = currentTime();
502 e.pid = info.pid;
503 e.protocol = info.protocol;
504 e.src_ip = info.src_ip;
505 e.host = info.dest_host;
506 e.ip = info.dest_ip;
507 e.port = info.dest_port;
508 e.args = argsStr;
509 // verdict resta "" — lo popoliamo alla risoluzione (vedi
510 // setLastVerdict sotto). pending=true se era un popup in volo.
511 e.verdict = pending ? "" : "";
512 e.pending = pending;
513 e.count = 1;
514 pushDetailEvent(app_display, e);
515 }
516
517 // 4) Vista detail: solo se siamo in detail e l'app matcha il filtro.
518 // Altrimenti la riga non viene aggiunta al m_store (il filtro live
519 // previene di sporcare la vista con app non pertinenti).
520 const bool in_detail_view = (m_stack.get_visible_child_name() == "detail");
521 if (in_detail_view && !m_currentAppFilter.empty()
522 && app_display == m_currentAppFilter) {
523 addOrUpdateDetailRow(info, app_display, argsStr, pending);
524 }
525}
526
527// ---------------------------------------------------------------------------
528// Aggregati per-app
529// ---------------------------------------------------------------------------
530
531void ConnectionsPage::updateAppAggregate(const std::string& app_display)
532{
533 const int64_t now_epoch = static_cast<int64_t>(std::time(nullptr));
534 auto& stats = m_appStats[app_display];
535 stats.conn_count += 1;
536 stats.estimated_bytes += kEstimatedBytesPerConn;
537 stats.last_seen_epoch = now_epoch;
538
539 auto idx_it = m_appIndex.find(app_display);
540 if (idx_it != m_appIndex.end()
541 && idx_it->second
542 && m_appStore->iter_is_valid(idx_it->second))
543 {
544 auto row = *(idx_it->second);
545 row[m_appColumns.col_conn_count] = stats.conn_count;
546 row[m_appColumns.col_bytes_raw] = stats.estimated_bytes;
547 row[m_appColumns.col_bytes_str] = formatBytes(stats.estimated_bytes);
549 row[m_appColumns.col_last_ts] = now_epoch;
550 } else {
551 auto it = m_appStore->append();
552 auto row = *it;
553 row[m_appColumns.col_icon_name] = resolveIconName(app_display);
554 row[m_appColumns.col_app] = app_display;
555 row[m_appColumns.col_conn_count] = stats.conn_count;
556 row[m_appColumns.col_bytes_raw] = stats.estimated_bytes;
557 row[m_appColumns.col_bytes_str] = formatBytes(stats.estimated_bytes);
559 row[m_appColumns.col_last_ts] = now_epoch;
560 m_appIndex[app_display] = it;
561 }
562}
563
564// ---------------------------------------------------------------------------
565// Ring buffer per-app degli ultimi 500 eventi detail (persistenza)
566// ---------------------------------------------------------------------------
567
568void ConnectionsPage::pushDetailEvent(const std::string& app_display,
569 const DetailEvent& ev)
570{
571 auto& ring = m_detailRings[app_display];
572 ring.push_back(ev);
573 // Enforce max size con eviction FIFO: se superiamo, scartiamo il
574 // piu' vecchio (front). Alternativa sarebbe ignorare il nuovo, ma
575 // gli eventi piu' recenti sono in genere quelli che interessano di
576 // piu' all'utente.
577 while (ring.size() > kDetailRingMax) {
578 ring.pop_front();
579 }
580}
581
582void ConnectionsPage::populateDetailFromRing(const std::string& app_display)
583{
584 auto it = m_detailRings.find(app_display);
585 if (it == m_detailRings.end()) return;
586
587 const auto& ring = it->second;
588 if (ring.empty()) return;
589
590 // PERF: stacchiamo il modello dalla TreeView prima dell'inserimento
591 // massivo. Con il modello ancora attaccato, GTK emette row-inserted
592 // e ridisegna per OGNI append — con fino a kDetailRingMax (500)
593 // eventi nel ring buffer, il click su un'app per aprirne il detail
594 // risultava percepibilmente lento (decine/centinaia di ms di lag
595 // prima che la vista comparisse). Staccando il modello facciamo un
596 // unico redraw alla fine, riportando il tempo a sostanzialmente
597 // istantaneo anche con la history piena. Stesso pattern usato da
598 // file manager / log viewer GTK per liste lunghe.
599 m_treeView.unset_model();
600
601 // Iteriamo dal piu' recente al piu' vecchio e prependiamo, cosi' il
602 // display ha lo stesso ordine live (piu' recente in cima).
603 for (auto r_it = ring.rbegin(); r_it != ring.rend(); ++r_it) {
604 const auto& e = *r_it;
605 auto row_it = m_store->append(); // append, non prepend: iteriamo reverse
606 auto row = *row_it;
607 row[m_columns.col_time] = e.time;
608 row[m_columns.col_app] = app_display;
609 row[m_columns.col_verdict] = e.verdict;
610 row[m_columns.col_count] = e.count;
611 row[m_columns.col_pid] = e.pid;
612 row[m_columns.col_protocol] = e.protocol;
613 row[m_columns.col_src_ip] = e.src_ip;
614 row[m_columns.col_host] = e.host;
615 row[m_columns.col_ip] = e.ip;
616 row[m_columns.col_port] = e.port;
617 row[m_columns.col_args] = e.args;
618 row[m_columns.col_pending] = e.pending;
619 // group_key vuoto: non popoliamo m_groupIndex con righe storiche
620 // per evitare che il dedup online colleghi pin fresco a riga
621 // ricostruita da log.
622 row[m_columns.col_group_key] = "";
623 }
624
625 // Ricolleghiamo il modello: un unico redraw per tutte le righe.
626 m_treeView.set_model(m_store);
627}
628
629// ---------------------------------------------------------------------------
630// Riga vista detail — con dedup per-gruppo come nella v2
631// ---------------------------------------------------------------------------
632
634 const std::string& app_display,
635 const std::string& argsStr,
636 bool pending)
637{
638 const std::string key = groupKeyFor(app_display, info.protocol,
639 info.dest_host, info.dest_ip,
640 info.dest_port);
641
642 // Dedup solo per righe non-pending
643 if (!key.empty() && !pending) {
644 auto it_idx = m_groupIndex.find(key);
645 if (it_idx != m_groupIndex.end()) {
646 auto it = it_idx->second;
647 if (it && m_store->iter_is_valid(it)) {
648 const bool row_pending = (*it)[m_columns.col_pending];
649 if (!row_pending) {
650 const uint32_t new_count = static_cast<uint32_t>((*it)[m_columns.col_count]) + 1;
651 const Glib::ustring v_app = (*it)[m_columns.col_app];
652 const Glib::ustring v_verdict = (*it)[m_columns.col_verdict];
653 const Glib::ustring v_proto = (*it)[m_columns.col_protocol];
654 const Glib::ustring v_src_ip = (*it)[m_columns.col_src_ip];
655 const Glib::ustring v_host = (*it)[m_columns.col_host];
656 const Glib::ustring v_ip = (*it)[m_columns.col_ip];
657 const uint32_t v_port = (*it)[m_columns.col_port];
658 const Glib::ustring v_args = (*it)[m_columns.col_args];
659 const Glib::ustring v_gkey = (*it)[m_columns.col_group_key];
660
661 m_store->erase(it);
662
663 auto newIt = m_store->prepend();
664 auto newRow = *newIt;
665 newRow[m_columns.col_time] = currentTime();
666 newRow[m_columns.col_app] = v_app;
667 newRow[m_columns.col_verdict] = v_verdict;
668 newRow[m_columns.col_count] = new_count;
669 newRow[m_columns.col_pid] = info.pid;
670 newRow[m_columns.col_protocol] = v_proto;
671 newRow[m_columns.col_src_ip] = v_src_ip;
672 newRow[m_columns.col_host] = v_host;
673 newRow[m_columns.col_ip] = v_ip;
674 newRow[m_columns.col_port] = v_port;
675 newRow[m_columns.col_args] = v_args;
676 newRow[m_columns.col_pending] = false;
677 newRow[m_columns.col_group_key] = v_gkey;
678
679 m_groupIndex[key] = newIt;
680 return;
681 }
682 } else {
683 m_groupIndex.erase(it_idx);
684 }
685 }
686 }
687
688 // Nuova riga
689 auto it = m_store->prepend();
690 auto row = *it;
692 row[m_columns.col_app] = app_display;
693 row[m_columns.col_verdict] = pending ? _("In attesa...") : "";
694 row[m_columns.col_count] = 1u;
695 row[m_columns.col_pid] = info.pid;
696 row[m_columns.col_protocol] = info.protocol;
697 row[m_columns.col_src_ip] = info.src_ip;
698 row[m_columns.col_host] = info.dest_host;
699 row[m_columns.col_ip] = info.dest_ip;
700 row[m_columns.col_port] = info.dest_port;
701 row[m_columns.col_args] = argsStr;
702 row[m_columns.col_pending] = pending;
703 row[m_columns.col_group_key] = key;
704
705 if (!key.empty()) m_groupIndex[key] = it;
706}
707
708// ---------------------------------------------------------------------------
709// Sparkline tick + draw
710// ---------------------------------------------------------------------------
711
713{
714 // Sposta il bucket corrente nella storia. Mantieni al massimo
715 // kSparkBucketsMax bucket; scarta il piu' vecchio in testa.
717 while (static_cast<int>(m_sparkBuckets.size()) > kSparkBucketsMax) {
718 m_sparkBuckets.pop_front();
719 }
720 m_currentBucket = 0;
721
722 // Aggiorna l'etichetta con il picco + media degli ultimi 60 s
723 uint64_t peak = 0, sum = 0;
724 for (auto b : m_sparkBuckets) {
725 if (b > peak) peak = b;
726 sum += b;
727 }
728 // Per avere "byte/s" moltiplichiamo per 1000/kSparkBucketMs = 2
729 const uint64_t avg_bps = (sum * 1000ULL) /
731 const uint64_t peak_bps = (peak * 1000ULL) / kSparkBucketMs;
732 m_sparkLabel.set_markup(
733 Glib::ustring::compose(
734 _("<b>Throughput stimato</b> <small>ultimi 60 s — media %1/s · picco %2/s (stima basata sul numero di connessioni)</small>"),
735 formatBytes(avg_bps),
736 formatBytes(peak_bps)));
737
738 m_sparkArea.queue_draw();
739 return true; // mantieni il timer attivo
740}
741
742void ConnectionsPage::onSparkDraw(const Cairo::RefPtr<Cairo::Context>& cr,
743 int width, int height)
744{
745 if (m_sparkBuckets.empty() || width <= 0 || height <= 0) return;
746
747 // Trova il massimo per normalizzare
748 uint64_t peak = 0;
749 for (auto b : m_sparkBuckets) if (b > peak) peak = b;
750 if (peak == 0) peak = 1; // evita div/0 quando tutto vuoto
751
752 const int n = static_cast<int>(m_sparkBuckets.size());
753 const double bar_w = static_cast<double>(width) / n;
754 const double pad = 4.0;
755
756 // Sfondo sottile (griglia a 25/50/75% dell'altezza)
757 cr->set_source_rgba(1, 1, 1, 0.06);
758 cr->set_line_width(1);
759 for (int i = 1; i < 4; ++i) {
760 double y = pad + (height - 2 * pad) * i / 4.0;
761 cr->move_to(0, y);
762 cr->line_to(width, y);
763 cr->stroke();
764 }
765
766 // Area riempita (verde BastionGuard morbido)
767 cr->set_source_rgba(0.30, 0.78, 0.55, 0.85);
768 cr->move_to(0, height - pad);
769 for (int i = 0; i < n; ++i) {
770 const double frac = static_cast<double>(m_sparkBuckets[i]) / peak;
771 const double x = i * bar_w;
772 const double y = (height - pad) - frac * (height - 2 * pad);
773 cr->line_to(x, y);
774 }
775 cr->line_to(width, height - pad);
776 cr->close_path();
777 cr->fill_preserve();
778
779 // Contorno superiore piu' marcato
780 cr->set_source_rgba(0.30, 0.78, 0.55, 1.0);
781 cr->set_line_width(1.5);
782 cr->stroke();
783}
784
785// ---------------------------------------------------------------------------
786// Verdict handling
787// ---------------------------------------------------------------------------
788
789void ConnectionsPage::setLastVerdict(const std::string& verdict)
790{
791 auto children = m_store->children();
792 for (auto it = children.begin(); it != children.end(); ++it) {
793 auto row = *it;
794 if (row[m_columns.col_pending]) {
795 row[m_columns.col_verdict] = verdict;
796 row[m_columns.col_pending] = false;
797 const Glib::ustring k = row[m_columns.col_group_key];
798 if (!k.empty()) m_groupIndex[k.raw()] = it;
799 return;
800 }
801 }
802 if (!children.empty()) {
803 auto first = *children.begin();
804 first[m_columns.col_verdict] = verdict;
805 }
806}
807
808void ConnectionsPage::onVerdictEdited(const Glib::ustring& pathStr,
809 const Glib::ustring& newText)
810{
811 auto it = m_store->get_iter(pathStr);
812 if (!it) return;
813
814 const bool wasPending = (*it)[m_columns.col_pending];
815 (*it)[m_columns.col_verdict] = newText;
816
817 if (wasPending) {
819 (*it)[m_columns.col_pending] = false;
820 }
821}
822
823ReplyAction ConnectionsPage::parseReplyAction(const Glib::ustring& text) const
824{
825 if (text == "allow once") return ReplyAction::ALLOW_ONCE;
826 if (text == "allow always") return ReplyAction::ALLOW_ALWAYS;
827 if (text == "deny always") return ReplyAction::DENY_ALWAYS;
829}
830
832{
833 clearLog();
834 // Dopo Pulisci vogliamo sempre ripartire dalla vista aggregata:
835 // 1) il filtro corrente ("Connessioni di firefox") diventa stale se
836 // siamo in detail — nuove connessioni di altre app non apparirebbero
837 // nel detail (per design), e il risultato sarebbe una schermata che
838 // sembra "rotta" con zero contenuti. L'utente si aspetta una
839 // panoramica fresca.
840 // 2) Se l'utente vuole rientrare nel detail di un'app, ora dopo il
841 // reset basta un doppio click sulla riga app appena ricreata.
842 if (m_stack.get_visible_child_name() == "detail") {
843 showAppsView();
844 }
845}
846
848{
849 auto now = Glib::DateTime::create_now_local();
850 return now.format("%H:%M:%S");
851}
852
853// ---------------------------------------------------------------------------
854// Log persistente JSON Lines — include anche aggregati per-app
855// ---------------------------------------------------------------------------
856
858{
859 return "/tmp/bastionguard-sc-connections-" +
860 std::to_string(::getuid()) + ".jsonl";
861}
862
863std::string ConnectionsPage::jsonEscapeStr(const std::string& s)
864{
865 std::string out;
866 for (unsigned char c : s) {
867 switch (c) {
868 case '"': out += "\\\""; break;
869 case '\\': out += "\\\\"; break;
870 case '\n': out += "\\n"; break;
871 case '\r': out += "\\r"; break;
872 case '\t': out += "\\t"; break;
873 default:
874 if (c < 0x20) { char b[8]; std::snprintf(b, sizeof(b), "\\u%04x", c); out += b; }
875 else out += c;
876 }
877 }
878 return out;
879}
880
881std::string ConnectionsPage::jsonFieldStr(const std::string& json, const std::string& key)
882{
883 const std::string needle = "\"" + key + "\":\"";
884 auto pos = json.find(needle);
885 if (pos == std::string::npos) return {};
886 pos += needle.size();
887 auto end = json.find('"', pos);
888 return end == std::string::npos ? std::string{} : json.substr(pos, end - pos);
889}
890
891uint32_t ConnectionsPage::jsonFieldUint(const std::string& json, const std::string& key)
892{
893 const std::string needle = "\"" + key + "\":";
894 auto pos = json.find(needle);
895 if (pos == std::string::npos) return 0;
896 pos += needle.size();
897 return static_cast<uint32_t>(std::strtoul(json.c_str() + pos, nullptr, 10));
898}
899
900bool ConnectionsPage::jsonFieldBool(const std::string& json, const std::string& key)
901{
902 const std::string needle = "\"" + key + "\":";
903 auto pos = json.find(needle);
904 if (pos == std::string::npos) return false;
905 pos += needle.size();
906 return json.substr(pos, 4) == "true";
907}
908
910{
911 std::ofstream f(logPath(), std::ios::trunc);
912 if (!f.is_open()) return;
913
914 // Persistiamo:
915 // - aggregati per-app (type="app") — per la vista "Applicazioni"
916 // - ultimi 500 eventi detail per app (type="conn") — per la vista
917 // dettagli quando l'utente rientra in un'app
918 //
919 // Retention 7 giorni enforced al loadLog(): il file cresce comunque
920 // proporzionalmente al kDetailRingMax (500 eventi × numero app),
921 // tipicamente <5 MB anche con uso intenso.
922 for (const auto& kv : m_appStats) {
923 const std::string& app = kv.first;
924 const AppStats& s = kv.second;
925 f << "{\"type\":\"app\""
926 << ",\"app\":\"" << jsonEscapeStr(app) << "\""
927 << ",\"count\":" << s.conn_count
928 << ",\"bytes\":" << s.estimated_bytes
929 << ",\"last_ts\":" << s.last_seen_epoch
930 << "}\n";
931 }
932
933 // Eventi detail: per ogni app scriviamo il suo ring buffer in ordine
934 // cronologico crescente (front = piu' vecchio, back = piu' recente).
935 for (const auto& [app, ring] : m_detailRings) {
936 for (const auto& e : ring) {
937 f << "{\"type\":\"conn\""
938 << ",\"app\":\"" << jsonEscapeStr(app) << "\""
939 << ",\"ts\":" << e.ts_epoch
940 << ",\"time\":\"" << jsonEscapeStr(e.time) << "\""
941 << ",\"pid\":" << e.pid
942 << ",\"proto\":\"" << jsonEscapeStr(e.protocol) << "\""
943 << ",\"src_ip\":\"" << jsonEscapeStr(e.src_ip) << "\""
944 << ",\"host\":\"" << jsonEscapeStr(e.host) << "\""
945 << ",\"ip\":\"" << jsonEscapeStr(e.ip) << "\""
946 << ",\"port\":" << e.port
947 << ",\"args\":\"" << jsonEscapeStr(e.args) << "\""
948 << ",\"verdict\":\"" << jsonEscapeStr(e.verdict) << "\""
949 << ",\"pending\":" << (e.pending ? "true" : "false")
950 << ",\"count\":" << e.count
951 << "}\n";
952 }
953 }
954}
955
957{
958 std::ifstream f(logPath());
959 if (!f.is_open()) return;
960
961 const int64_t now_epoch = static_cast<int64_t>(std::time(nullptr));
962 const int64_t cutoff = now_epoch - kRetentionSeconds;
963 int n_apps = 0, n_conns = 0, n_dropped = 0;
964
965 // Helper parser che gestiscono uint64/int64 (jsonFieldUint e' solo u32)
966 auto parse_u64 = [](const std::string& line, const char* key) -> uint64_t {
967 const std::string needle = std::string("\"") + key + "\":";
968 auto pos = line.find(needle);
969 if (pos == std::string::npos) return 0;
970 pos += needle.size();
971 return std::strtoull(line.c_str() + pos, nullptr, 10);
972 };
973 auto parse_i64 = [](const std::string& line, const char* key) -> int64_t {
974 const std::string needle = std::string("\"") + key + "\":";
975 auto pos = line.find(needle);
976 if (pos == std::string::npos) return 0;
977 pos += needle.size();
978 return std::strtoll(line.c_str() + pos, nullptr, 10);
979 };
980
981 std::string line;
982 while (std::getline(f, line)) {
983 if (line.empty() || line[0] != '{') continue;
984
985 const std::string type = jsonFieldStr(line, "type");
986
987 if (type == "app") {
988 // Ripristina aggregato per-app (caso v3)
989 const std::string app = jsonFieldStr(line, "app");
990 if (app.empty()) continue;
991 const int64_t ts = parse_i64(line, "last_ts");
992 // Retention 7 giorni: aggregato obsoleto se l'ultima
993 // attivita' e' oltre il cutoff.
994 if (ts > 0 && ts < cutoff) { ++n_dropped; continue; }
995 AppStats s;
996 s.conn_count = jsonFieldUint(line, "count");
997 s.estimated_bytes = parse_u64(line, "bytes");
998 s.last_seen_epoch = ts;
999 m_appStats[app] = s;
1000 ++n_apps;
1001 continue;
1002 }
1003
1004 if (type == "conn") {
1005 // Evento detail persistito: lo rimettiamo nel ring buffer
1006 // dell'app corrispondente (se fresco).
1007 const std::string app = jsonFieldStr(line, "app");
1008 if (app.empty()) continue;
1009 const int64_t ts = parse_i64(line, "ts");
1010 if (ts > 0 && ts < cutoff) { ++n_dropped; continue; }
1011
1012 DetailEvent e;
1013 e.ts_epoch = ts;
1014 e.time = jsonFieldStr(line, "time");
1015 e.pid = jsonFieldUint(line, "pid");
1016 e.protocol = jsonFieldStr(line, "proto");
1017 e.src_ip = jsonFieldStr(line, "src_ip");
1018 e.host = jsonFieldStr(line, "host");
1019 e.ip = jsonFieldStr(line, "ip");
1020 e.port = jsonFieldUint(line, "port");
1021 e.args = jsonFieldStr(line, "args");
1022 e.verdict = jsonFieldStr(line, "verdict");
1023 e.pending = jsonFieldBool(line, "pending");
1024 e.count = jsonFieldUint(line, "count");
1025 if (e.count == 0) e.count = 1;
1026 // Append in coda: preserviamo l'ordine cronologico del file
1027 // (e' scritto cronologico crescente). Se il ring e' pieno
1028 // sconfiniamo a kDetailRingMax — via pushDetailEvent.
1029 pushDetailEvent(app, e);
1030 ++n_conns;
1031 continue;
1032 }
1033
1034 // Log legacy v2 senza "type": trattalo come conn per ricostruire
1035 // almeno il totale aggregato.
1036 const std::string app = jsonFieldStr(line, "app");
1037 if (app.empty()) continue;
1038 AppStats& s = m_appStats[app];
1039 s.conn_count += 1;
1041 if (s.last_seen_epoch == 0) s.last_seen_epoch = now_epoch;
1042 }
1043
1044 // Popola la ListStore apps a partire dagli aggregati accumulati.
1045 for (const auto& kv : m_appStats) {
1046 auto it = m_appStore->append();
1047 auto row = *it;
1049 row[m_appColumns.col_app] = kv.first;
1050 row[m_appColumns.col_conn_count] = kv.second.conn_count;
1051 row[m_appColumns.col_bytes_raw] = kv.second.estimated_bytes;
1052 row[m_appColumns.col_bytes_str] = formatBytes(kv.second.estimated_bytes);
1053 row[m_appColumns.col_last_seen] = formatEpochTime(kv.second.last_seen_epoch);
1054 row[m_appColumns.col_last_ts] = kv.second.last_seen_epoch;
1055 m_appIndex[kv.first] = it;
1056 }
1057
1058 std::cerr << "[ConnectionsPage] Caricato log: "
1059 << n_apps << " app + " << n_conns << " eventi detail, "
1060 << n_dropped << " scartati per retention 7gg ("
1061 << m_appStats.size() << " app uniche totali)\n";
1062}
1063
1065{
1066 m_store->clear();
1067 m_groupIndex.clear();
1068 m_appStore->clear();
1069 m_appIndex.clear();
1070 m_appStats.clear();
1071 m_detailRings.clear();
1072 std::error_code ec;
1073 std::filesystem::remove(logPath(), ec);
1074}
nlohmann::json json
Definition Backend.cpp:2908
#define _(s)
static const char * kVerdictOptions[]
ReplyAction
Definition UIServer.hpp:42
void onAppActivated(const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column)
std::deque< uint64_t > m_sparkBuckets
Gtk::TreeView m_appTreeView
Gtk::ScrolledWindow m_scroll
std::unordered_map< std::string, std::string > m_desktopIndex
Gtk::TreeView m_treeView
Gtk::ScrolledWindow m_appScroll
ConnectionsPage(UIServer &uiServer)
DetailColumns m_columns
Glib::RefPtr< Gtk::ListStore > m_appStore
void onSparkDraw(const Cairo::RefPtr< Cairo::Context > &cr, int width, int height)
static constexpr uint64_t kEstimatedBytesPerConn
void setLastVerdict(const std::string &verdict)
void pushDetailEvent(const std::string &app_display, const DetailEvent &ev)
static std::string groupKeyFor(const std::string &app, const std::string &protocol, const std::string &host, const std::string &ip, uint32_t port)
Gtk::DrawingArea m_sparkArea
std::string currentTime()
Glib::RefPtr< Gtk::ListStore > m_store
void onVerdictEdited(const Glib::ustring &pathStr, const Glib::ustring &newText)
static std::string formatEpochTime(int64_t epoch)
std::unordered_map< std::string, Gtk::TreeModel::iterator > m_appIndex
Glib::RefPtr< Gtk::ListStore > m_verdictModel
std::unordered_map< std::string, std::deque< DetailEvent > > m_detailRings
static std::string formatBytes(uint64_t n)
std::unordered_map< std::string, AppStats > m_appStats
static std::string jsonEscapeStr(const std::string &s)
static bool jsonFieldBool(const std::string &json, const std::string &key)
sigc::connection m_sparkTick
void onNewConnection(const ConnectionInfo &info, bool pending=true)
static std::string jsonFieldStr(const std::string &json, const std::string &key)
~ConnectionsPage() override
std::string m_currentAppFilter
static constexpr int kSparkBucketMs
void populateDetailFromRing(const std::string &app_display)
void updateAppAggregate(const std::string &app_display)
void addOrUpdateDetailRow(const ConnectionInfo &info, const std::string &app_display, const std::string &argsStr, bool pending)
std::unordered_map< std::string, Gtk::TreeModel::iterator > m_groupIndex
void showDetailView(const std::string &app)
static constexpr int kSparkBucketsMax
ReplyAction parseReplyAction(const Glib::ustring &text) const
Gtk::SearchEntry m_search
static constexpr int64_t kRetentionSeconds
static std::string logPath()
static constexpr size_t kDetailRingMax
std::string resolveIconName(const std::string &app_display)
static uint32_t jsonFieldUint(const std::string &json, const std::string &key)
void sendReply(ReplyAction action)
Definition UIServer.cpp:422
std::string safe_local_process_icon_name()
std::string safe_local_process_label()
std::string best_connection_display_name(const std::string &app_comm, const std::string &app_path, const std::vector< std::string > &app_args, const std::string &dst_ip, const std::string &src_ip)
std::string best_app_icon_name(const std::string &app_comm, const std::string &app_path, const std::vector< std::string > &app_args)
uint32_t pid
Definition UIServer.hpp:31
std::string src_ip
Definition UIServer.hpp:35
std::string protocol
Definition UIServer.hpp:34
std::string dest_host
Definition UIServer.hpp:37
uint32_t dest_port
Definition UIServer.hpp:39
std::string app_path
Definition UIServer.hpp:27
std::vector< std::string > app_args
Definition UIServer.hpp:30
std::string dest_ip
Definition UIServer.hpp:38
std::string app_comm
Definition UIServer.hpp:28
Gtk::TreeModelColumn< Glib::ustring > col_icon_name
Gtk::TreeModelColumn< int64_t > col_last_ts
Gtk::TreeModelColumn< Glib::ustring > col_last_seen
Gtk::TreeModelColumn< uint32_t > col_conn_count
Gtk::TreeModelColumn< uint64_t > col_bytes_raw
Gtk::TreeModelColumn< Glib::ustring > col_bytes_str
Gtk::TreeModelColumn< Glib::ustring > col_app
Gtk::TreeModelColumn< Glib::ustring > val
Gtk::TreeModelColumn< Glib::ustring > col_protocol
Gtk::TreeModelColumn< Glib::ustring > col_ip
Gtk::TreeModelColumn< Glib::ustring > col_src_ip
Gtk::TreeModelColumn< Glib::ustring > col_group_key
Gtk::TreeModelColumn< uint32_t > col_port
Gtk::TreeModelColumn< Glib::ustring > col_time
Gtk::TreeModelColumn< Glib::ustring > col_args
Gtk::TreeModelColumn< Glib::ustring > col_host
Gtk::TreeModelColumn< bool > col_pending
Gtk::TreeModelColumn< Glib::ustring > col_verdict
Gtk::TreeModelColumn< Glib::ustring > col_app
Gtk::TreeModelColumn< uint32_t > col_count
Gtk::TreeModelColumn< uint32_t > col_pid