Skip to content
API Documentation
BastionGuard™
PrivacyPage.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
21#include "SafeProcess.hpp"
22#include "PrivacyPage.hpp"
23#include "Resource.hpp"
24#include <filesystem>
25#include <fstream>
26#include <iostream>
27#include <thread>
28#include <queue>
29#include <set>
30#include <atomic>
31#include <csignal>
32#include <unistd.h>
33#include <cerrno>
34#include <sys/wait.h>
35#include <glibmm/i18n.h>
36#include <glibmm/miscutils.h>
37#include <glibmm/main.h>
38#include <glibmm/keyfile.h>
39#include <glibmm/fileutils.h>
40
41namespace fs = std::filesystem;
42
43std::queue<std::string> alert_queue_;
44Gtk::Window* current_alert_ = nullptr;
45
46
47static bool run_privhelper_pkexec(const std::string& action) {
48 if (action != "block" && action != "unblock")
49 return false;
50
51 const pid_t pid = ::fork();
52 if (pid < 0)
53 return false;
54
55 if (pid == 0) {
56 ::execl(
57 "/usr/bin/pkexec",
58 "pkexec",
59 "/usr/bin/bastionguard-privhelper",
60 action.c_str(),
61 static_cast<char*>(nullptr)
62 );
63 _exit(127);
64 }
65
66 int status = 0;
67 while (::waitpid(pid, &status, 0) < 0) {
68 if (errno == EINTR)
69 continue;
70 return false;
71 }
72
73 return WIFEXITED(status) && WEXITSTATUS(status) == 0;
74}
75
76
77static std::string read_text_file(const std::string& path) {
78 std::ifstream f(path);
79 if (!f.is_open()) return {};
80 std::string s;
81 std::getline(f, s);
82 return s;
83}
84
85static std::string read_cmdline(const std::string& path) {
86 std::ifstream f(path, std::ios::binary);
87 if (!f.is_open()) return {};
88 std::string buf((std::istreambuf_iterator<char>(f)), {});
89 for (char& c : buf) if (c == '\0') c = ' ';
90 return buf;
91}
92
93static std::string read_exe_path(const std::string& pid_str) {
94 std::error_code ec;
95 auto p = fs::read_symlink("/proc/" + pid_str + "/exe", ec);
96 if (ec) return {};
97 return p.string();
98}
99
100static bool is_noise_process(const std::string& comm,
101 const std::string& exe,
102 const std::string& cmdline)
103{
104 // Match per nome (comm)
105 static const std::set<std::string> kCommDeny = {
106 "bash","zsh","fish","sh","dash",
107 "konsole","gnome-terminal-","xfce4-terminal","kitty","alacritty","wezterm",
108 "tmux","screen",
109 "pkexec",
110 "polkitd","polkit-agent-helper-1","polkit-kde-authentication-agent-1",
111 "gnome-shell","plasmashell","kwin_wayland","kwin_x11",
112 "xdg-desktop-portal","xdg-desktop-portal-gtk","xdg-desktop-portal-kde",
113 "pipewire","wireplumber","pulseaudio"
114 };
115
116 if (!comm.empty() && kCommDeny.count(comm)) return true;
117
118 // Match per path exe (più stabile di comm)
119 auto has = [&](const char* needle) {
120 return (!exe.empty() && exe.find(needle) != std::string::npos) ||
121 (!cmdline.empty() && cmdline.find(needle) != std::string::npos);
122 };
123
124 // Polkit / privilege prompts
125 if (has("polkit") || has("pkexec")) return true;
126
127 // Terminali / shell
128 if (has("konsole") || has("gnome-terminal") || has("xfce4-terminal") ||
129 has("kitty") || has("alacritty") || has("wezterm") ||
130 has("/bin/bash") || has("/usr/bin/bash") || has("/bin/zsh") || has("/usr/bin/zsh") ||
131 has("/bin/fish") || has("/usr/bin/fish"))
132 return true;
133
134 // Portali / media stack (spesso fanno probing o broker)
135 if (has("xdg-desktop-portal") || has("pipewire") || has("wireplumber") || has("pulseaudio"))
136 return true;
137
138 // Desktop env “core”
139 if (has("plasmashell") || has("gnome-shell") || has("kwin"))
140 return true;
141
142 return false;
143}
144
145static int read_ppid(const std::string& pid_str) {
146 std::ifstream f("/proc/" + pid_str + "/status");
147 if (!f.is_open()) return -1;
148
149 std::string line;
150 while (std::getline(f, line)) {
151 if (line.rfind("PPid:", 0) == 0) {
152 try {
153 // dopo "PPid:" ci sono tab/spazi -> stoi li gestisce
154 return std::stoi(line.substr(5));
155 } catch (...) {
156 return -1;
157 }
158 }
159 }
160 return -1;
161}
162
163
164static bool parent_is_noise(const std::string& pid_str) {
165 std::string cur = pid_str;
166 for (int i = 0; i < 3; ++i) { // risali max 3 livelli
167 int ppid = read_ppid(cur);
168 if (ppid <= 1) break;
169 std::string p = std::to_string(ppid);
170 std::string comm = read_text_file("/proc/" + p + "/comm");
171 std::string exe = read_exe_path(p);
172 std::string cmd = read_cmdline("/proc/" + p + "/cmdline");
173 if (is_noise_process(comm, exe, cmd)) return true;
174 cur = p;
175 }
176 return false;
177}
178
179
181 : Gtk::Box(Gtk::Orientation::VERTICAL, 12)
182{
183 set_margin(16);
184 set_vexpand(true);
185
186
187 auto title_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
188 title_box->add_css_class("secure-headerbar");
189 title_box->set_margin_bottom(8);
190
191 lbl_title_.set_text(_("🔒 Privacy Webcam / Microfono"));
192 lbl_title_.set_margin(8);
193 lbl_title_.set_halign(Gtk::Align::CENTER);
194 lbl_title_.set_hexpand(true);
195 lbl_title_.add_css_class("secure-header-title");
196 title_box->append(lbl_title_);
197 append(*title_box);
198
199
200 bool last_state = load_privacy_state();
201 switch_webcam_.set_active(last_state);
202
203 try {
204 std::string config_path = Glib::get_home_dir() + "/.config/BastionGuard/privacy.conf";
205 if (Glib::file_test(config_path, Glib::FileTest::EXISTS)) {
206 auto keyfile = Glib::KeyFile::create();
207 keyfile->load_from_file(config_path);
208 bool auto_refresh_enabled = false;
209 if (keyfile->has_key("privacy", "auto_refresh"))
210 auto_refresh_enabled = keyfile->get_boolean("privacy", "auto_refresh");
211 chk_auto_refresh_.set_active(auto_refresh_enabled);
212 }
213 } catch (...) {
214 std::cerr << _("[Privacy] Errore lettura auto_refresh") << std::endl;
215 }
216
217 switch_webcam_.set_halign(Gtk::Align::CENTER);
218 switch_webcam_.set_valign(Gtk::Align::CENTER);
219 switch_webcam_.set_margin_bottom(6);
220 append(switch_webcam_);
221
222 auto lbl_switch = Gtk::make_managed<Gtk::Label>(_("Abilita/Disabilita Webcam e Microfono"));
223 lbl_switch->set_halign(Gtk::Align::CENTER);
224 append(*lbl_switch);
225
226
227 auto control_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
228 control_box->set_halign(Gtk::Align::CENTER);
229
230 btn_refresh_.set_label(_("🔄 Aggiorna"));
231 btn_refresh_.signal_clicked().connect(sigc::mem_fun(*this, &PrivacyPage::on_refresh_clicked));
232 control_box->append(btn_refresh_);
233
234 chk_auto_refresh_.set_label(_("Aggiornamento automatico ogni 5 minuti"));
235 chk_auto_refresh_.signal_toggled().connect(sigc::mem_fun(*this, &PrivacyPage::setup_auto_refresh_toggle));
236 control_box->append(chk_auto_refresh_);
237 append(*control_box);
238
239 btn_unlock_.set_label(_("🔓 Sblocca dispositivi"));
240 btn_unlock_.add_css_class("btn-warning");
241 btn_unlock_.set_halign(Gtk::Align::END);
242 btn_unlock_.set_margin_top(10);
243 btn_unlock_.set_visible(false);
244
245 btn_unlock_.signal_clicked().connect([this]() {
246 write_log(_("Richiesto sblocco manuale dei dispositivi"));
248 });
249
250
251 append(btn_unlock_);
252
253
254 scroller_.set_vexpand(true);
255 scroller_.set_hexpand(true);
256 txt_processes_.set_editable(false);
257 txt_processes_.set_wrap_mode(Gtk::WrapMode::WORD_CHAR);
258 scroller_.set_child(txt_processes_);
259 append(scroller_);
260
261
262 lbl_status_.set_margin_top(8);
263 lbl_status_.set_halign(Gtk::Align::CENTER);
264 append(lbl_status_);
265
266 {
267 auto alive = alive_;
268 Glib::signal_idle().connect_once([this, alive]() {
269 BastionGuard::if_alive(alive, [this]() {
270 try {
271 std::string base_dir = Glib::get_home_dir() + "/.local/share/BastionGuard/logs";
272 auto t = std::time(nullptr);
273 std::tm tm{};
274 localtime_r(&t, &tm); // thread-safe rispetto a std::localtime
275 char datebuf[16];
276 std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d", &tm);
277 std::string log_path = base_dir + "/privacyd_" + std::string(datebuf) + ".log";
278
279 if (fs::exists(log_path)) {
280 std::ifstream log_file(log_path);
281 std::stringstream buffer;
282 buffer << log_file.rdbuf();
283 txt_processes_.get_buffer()->set_text(buffer.str());
284 lbl_status_.set_text(_("📜 Log giornaliero caricato."));
285 } else {
286 lbl_status_.set_text(_("📝 Nessun log disponibile per oggi."));
287 }
288 } catch (...) {
289 lbl_status_.set_text(_("⚠️ Errore durante il caricamento del log."));
290 }
291 });
292 });
293 }
294
295
296 switch_webcam_.property_active().signal_changed().connect(
297 sigc::mem_fun(*this, &PrivacyPage::on_toggle_privacy));
298
299
300 {
301 auto alive_ctor = alive_;
302 Glib::signal_idle().connect_once([this, alive_ctor]() {
303 if (!alive_ctor || !alive_ctor->load(std::memory_order_acquire)) return;
304 lbl_status_.set_text(_("⏳ Inizializzazione del servizio privacy..."));
305 write_log(_("=== Avvio BastionGuard PrivacyPage ==="));
306 auto alive = alive_;
307 std::thread([this, alive]() {
308 if (!alive || !alive->load(std::memory_order_acquire)) return;
309 if (!is_daemon_active())
312 Glib::signal_idle().connect_once([this, alive]() {
313 BastionGuard::if_alive(alive, [this]() {
315 lbl_status_.set_text(_("🕵️ Monitoraggio privacy attivo"));
316 monitor_running_ = true;
318 });
319 });
320 }).detach();
321 });
322 }
323}
324
326 // Segnala ai lambda in volo (signal_idle + thread detached) che `this`
327 // sta per sparire: faranno early-return invece di fare use-after-free.
328 if (alive_) alive_->store(false, std::memory_order_release);
329
330 monitor_running_ = false;
331 if (monitor_thread_.joinable())
332 monitor_thread_.join();
333 write_log(_("=== Arresto monitoraggio privacy ==="));
334}
335
336
338 int rc = (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "is-active", "BastionGuard-privacyd.service"}).exit_code);
339 return (WIFEXITED(rc) && WEXITSTATUS(rc) == 0);
340}
341
343 write_log(_("Avvio automatico del servizio BastionGuard-privacyd..."));
344 (bg_process::run({"/usr/libexec/bastionguard/bastionguard-service", "--user", "start", "BastionGuard-privacyd.service"}).exit_code);
345}
346
347
349 if (run_privhelper_pkexec("unblock"))
350 write_log(_("📷 Webcam/Microfono sbloccati automaticamente"));
351 else
352 write_log(_("❌ Errore nello sblocco automatico"));
353}
354
355
356std::vector<std::string> PrivacyPage::detect_active_devices() {
357 std::vector<std::string> result;
358 const std::vector<std::string> prefixes = {"/dev/video", "/dev/snd/pcm"};
359
360 // Usiamo l'overload con std::error_code per NON lanciare eccezioni:
361 // /proc è intrinsecamente race-y (i PID possono sparire in qualsiasi momento)
362 // e una filesystem_error non gestita in questo thread causerebbe std::terminate().
363 std::error_code ec;
364 fs::directory_iterator proc_it("/proc", ec);
365 if (ec) {
366 // Impossibile aprire /proc: restituiamo una lista vuota anziché crashare.
367 return result;
368 }
369
370 for (; proc_it != fs::directory_iterator(); proc_it.increment(ec)) {
371 if (ec) {
372 // Errore avanzando l'iteratore (es. PID scomparso): fermiamo il ciclo
373 // in modo pulito, i device attivi verranno rilevati al prossimo giro.
374 break;
375 }
376
377 const auto& pid_entry = *proc_it;
378
379 std::error_code ec_dir;
380 if (!pid_entry.is_directory(ec_dir) || ec_dir) continue;
381
382 const std::string pid_str = pid_entry.path().filename().string();
383 if (!std::all_of(pid_str.begin(), pid_str.end(), ::isdigit)) continue;
384
385 const std::string fd_path = "/proc/" + pid_str + "/fd";
386 std::error_code ec_exists;
387 if (!fs::exists(fd_path, ec_exists) || ec_exists) continue;
388
389 // Lazy: carichiamo info processo solo se troviamo un fd interessante
390 std::string comm, exe, cmdline;
391 bool proc_info_loaded = false;
392
393 // Anche qui: overload con error_code. Se il processo muore tra exists()
394 // e la creazione dell'iteratore, non vogliamo un'eccezione non gestita.
395 std::error_code ec_fd;
396 fs::directory_iterator fd_it(
397 fd_path,
398 fs::directory_options::skip_permission_denied,
399 ec_fd);
400 if (ec_fd) continue;
401
402 for (; fd_it != fs::directory_iterator(); fd_it.increment(ec_fd)) {
403 if (ec_fd) break; // pid scomparso mentre iteravamo: skippa questo PID
404
405 const auto& fd_entry = *fd_it;
406
407 std::error_code ec_link;
408 auto link_target = fs::read_symlink(fd_entry.path(), ec_link);
409 if (ec_link) continue;
410 std::string target = link_target.string();
411
412 bool matches = false;
413 for (const auto& prefix : prefixes) {
414 if (target.rfind(prefix, 0) == 0) { // starts_with(prefix)
415 matches = true;
416 break;
417 }
418 }
419 if (!matches) continue;
420
421 if (!proc_info_loaded) {
422 comm = read_text_file("/proc/" + pid_str + "/comm");
423 if (comm.empty()) comm = "(sconosciuto)";
424 exe = read_exe_path(pid_str);
425 cmdline = read_cmdline("/proc/" + pid_str + "/cmdline");
426 proc_info_loaded = true;
427 }
428
429 // Soppressione popup per terminali/polkit/portal/pipewire ecc.
430 if (is_noise_process(comm, exe, cmdline)) break;
431
432 // Soppressione anche se un parent (fino a 3 livelli) è un processo "noise"
433 if (parent_is_noise(pid_str)) break;
434
435 result.push_back(comm + " (PID=" + pid_str + ")");
436 break; // trovato un accesso rilevante per questo PID, passa al prossimo PID
437 }
438 }
439
440 std::sort(result.begin(), result.end());
441 result.erase(std::unique(result.begin(), result.end()), result.end());
442 return result;
443}
444
445
446
447
449 monitor_thread_ = std::thread(&PrivacyPage::monitor_loop, this);
450 write_log(_("🕵️ Monitoraggio in tempo reale avviato"));
451}
452
454 while (monitor_running_) {
455 try {
456 auto processes = detect_active_devices();
457 for (const auto &proc : processes) {
458 if (!known_processes_.count(proc)) {
459 known_processes_.insert(proc);
460 write_log(Glib::ustring::compose(_("Nuovo accesso rilevato: %1"), proc));
461 auto alive = alive_;
462 Glib::signal_idle().connect_once([this, alive, proc]() {
463 BastionGuard::if_alive(alive, [this, &proc]() {
465 });
466 });
467 }
468 }
469 for (auto it = known_processes_.begin(); it != known_processes_.end();) {
470 if (std::find(processes.begin(), processes.end(), *it) == processes.end())
471 it = known_processes_.erase(it);
472 else ++it;
473 }
474 } catch (const std::exception& e) {
475 // Difesa in profondità: un'eccezione non catturata qui ucciderebbe il thread
476 // (std::terminate -> SIGABRT). La logghiamo e proseguiamo al prossimo giro.
477 try {
478 write_log(std::string(_("⚠️ Errore in monitor_loop: ")) + e.what());
479 } catch (...) { /* nulla di più da fare */ }
480 } catch (...) {
481 try {
482 write_log(_("⚠️ Errore sconosciuto in monitor_loop"));
483 } catch (...) { /* nulla di più da fare */ }
484 }
485 std::this_thread::sleep_for(std::chrono::seconds(3));
486 }
487}
488
489
490void PrivacyPage::show_permission_popup(const std::string &process_name) {
491 alert_queue_.push(process_name);
492 if (!current_alert_) {
493 auto alive = alive_;
494 Glib::signal_idle().connect_once([this, alive]() {
495 BastionGuard::if_alive(alive, [this]() { show_next_alert(); });
496 });
497 }
498}
499
501 if (alert_queue_.empty() || current_alert_)
502 return;
503
504 std::string proc = alert_queue_.front();
505 alert_queue_.pop();
506 size_t pending = alert_queue_.size();
507
508 current_alert_ = Gtk::make_managed<Gtk::Window>();
509 auto *win = current_alert_;
510
511 win->set_title("");
512 win->set_default_size(480, 230);
513 win->set_resizable(false);
514 win->set_modal(false);
515 win->add_css_class("BastionGuard-popup");
516 win->add_css_class("BastionGuard-dialog");
517
518
519 if (auto* root = dynamic_cast<Gtk::Window*>(get_root()))
520 win->set_transient_for(*root);
521
522
523 auto *header = Gtk::make_managed<Gtk::HeaderBar>();
524 header->set_show_title_buttons(false);
525 header->set_decoration_layout("");
526 header->add_css_class("custom-headerbar");
527
528
529 auto *left = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
530 left->set_size_request(240, -1);
531
532 auto *title_lbl = Gtk::make_managed<Gtk::Label>(_("⚠️ Accesso dispositivo rilevato"));
533 title_lbl->add_css_class("BastionGuard-title");
534 title_lbl->set_halign(Gtk::Align::START);
535 left->append(*title_lbl);
536
537 header->pack_start(*left);
538
539
540 auto *center = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
541 center->set_hexpand(true);
542 header->pack_start(*center);
543
544
545 auto *right = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
546
547 try {
548 auto pmin = Gdk::Pixbuf::create_from_file(resource("icon-minimize.png"), 28, 28, true);
549 auto *btn_min = Gtk::make_managed<Gtk::Button>();
550 btn_min->set_child(*Gtk::make_managed<Gtk::Image>(pmin));
551 btn_min->add_css_class("header-button");
552 btn_min->signal_clicked().connect([win]() { win->minimize(); });
553 right->append(*btn_min);
554 } catch (...) {}
555
556 try {
557 auto pclose = Gdk::Pixbuf::create_from_file(resource("icon-close.png"), 28, 28, true);
558 auto *btn_close = Gtk::make_managed<Gtk::Button>();
559 btn_close->set_child(*Gtk::make_managed<Gtk::Image>(pclose));
560 btn_close->add_css_class("header-button");
561 btn_close->signal_clicked().connect([this, win]() {
562 win->close();
563 current_alert_ = nullptr;
564 auto alive = alive_;
565 Glib::signal_idle().connect_once([this, alive]() {
566 BastionGuard::if_alive(alive, [this]() { show_next_alert(); });
567 });
568 });
569 right->append(*btn_close);
570 } catch (...) {}
571
572 header->pack_end(*right);
573
574 win->set_titlebar(*header);
575
576
577 auto *vbox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 12);
578 vbox->set_margin(20);
579 win->set_child(*vbox);
580
581 auto *msg = Gtk::make_managed<Gtk::Label>(
582 Glib::ustring::compose(
583 _("<b>%1</b> sta tentando di accedere a webcam o microfono."),
584 proc
585 )
586 );
587 msg->set_use_markup(true);
588 msg->set_wrap(true);
589 msg->set_halign(Gtk::Align::CENTER);
590 vbox->append(*msg);
591
592 if (pending > 0) {
593 auto *lbl_pending = Gtk::make_managed<Gtk::Label>(
594 Glib::ustring::compose(_("🕒 %1 altri processi in attesa..."), pending));
595 lbl_pending->set_halign(Gtk::Align::CENTER);
596 vbox->append(*lbl_pending);
597 }
598
599
600 auto *btn_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 12);
601 btn_box->set_halign(Gtk::Align::CENTER);
602
603 auto *btn_allow = Gtk::make_managed<Gtk::Button>(_("✔ Consenti"));
604 auto *btn_deny = Gtk::make_managed<Gtk::Button>(_("✖ Nega"));
605
606 btn_allow->add_css_class("btn-primary");
607 btn_deny->add_css_class("btn-danger");
608
609 btn_box->append(*btn_allow);
610 btn_box->append(*btn_deny);
611 vbox->append(*btn_box);
612
613 btn_allow->signal_clicked().connect([this, proc, win]() {
614 write_log(Glib::ustring::compose(_("Accesso CONSENTITO a %1"), proc));
615
616 win->close();
617 current_alert_ = nullptr;
618
619 auto alive = alive_;
620 Glib::signal_idle().connect_once([this, alive]() {
621 BastionGuard::if_alive(alive, [this]() { show_next_alert(); });
622 });
623 });
624
625
626 btn_deny->signal_clicked().connect([this, proc, win]() {
627
628 write_log(Glib::ustring::compose(
629 _("Accesso NEGATO a %1 — blocco webcam/microfono"), proc));
630
631
632 devices_blocked_ = true;
633
634
635 btn_unlock_.set_visible(true);
636
637
638 auto alive = alive_;
639 std::thread([this, alive]() {
640 const bool ok = run_privhelper_pkexec("block");
641
642 Glib::signal_idle().connect_once([this, alive, ok]() {
643 BastionGuard::if_alive(alive, [this, ok]() {
644 if (!ok) {
645 write_log(_("❌ ERRORE: impossibile applicare il blocco tramite helper!"));
646 lbl_status_.set_text(_("❌ Errore: impossibile bloccare webcam/microfono"));
647 } else {
648 write_log(_("🔒 Webcam/microfono BLOCCATI tramite helper (popup)"));
649 lbl_status_.set_text(_("🔒 Webcam e microfono bloccati"));
650 }
651
653 });
654 });
655 }).detach();
656
657
658 win->close();
659 current_alert_ = nullptr;
660 {
661 auto alive2 = alive_;
662 Glib::signal_idle().connect_once([this, alive2]() {
663 BastionGuard::if_alive(alive2, [this]() { show_next_alert(); });
664 });
665 }
666 });
667
668
669 win->present();
670}
671
672
674 lbl_status_.set_text(_("🔄 Aggiornamento elenco processi e log..."));
675 write_log(_("Aggiornamento manuale avviato"));
677}
678
679
680
682 bool enabled = chk_auto_refresh_.get_active();
683
684
685 try {
686 auto keyfile = Glib::KeyFile::create();
687 std::string config_dir = Glib::get_home_dir() + "/.config/BastionGuard";
688 std::string config_path = config_dir + "/privacy.conf";
689
690 if (!fs::exists(config_dir))
691 fs::create_directories(config_dir);
692
693 if (Glib::file_test(config_path, Glib::FileTest::EXISTS))
694 keyfile->load_from_file(config_path);
695
696 keyfile->set_boolean("privacy", "auto_refresh", enabled);
697 keyfile->save_to_file(config_path);
698 write_log(enabled ? _("Auto aggiornamento attivato (salvato in config)")
699 : _("Auto aggiornamento disattivato (salvato in config)"));
700 } catch (const Glib::Error& e) {
701 std::cerr << _("[Privacy] Errore salvataggio auto_refresh: ") << e.what() << std::endl;
702 }
703
704
705 if (enabled) {
706 lbl_status_.set_text(_("⏳ Auto aggiornamento attivo ogni 5 minuti."));
707 auto_refresh_conn_ = Glib::signal_timeout().connect_seconds(
708 [this]() {
709 // update_process_list() lancia internamente un thread protetto
710 // dall'alive flag, quindi è sicuro anche se la pagina muore.
712 return chk_auto_refresh_.get_active();
713 },
714 300);
715 } else {
716 lbl_status_.set_text(_("🛑 Auto aggiornamento disattivato."));
717 auto_refresh_conn_.disconnect();
718 }
719}
720
721
722
724 auto alive = alive_;
725 std::thread([this, alive]() {
726 if (!alive || !alive->load(std::memory_order_acquire)) return;
727 try {
728
729 auto processes = detect_active_devices();
730 std::ostringstream oss;
731 if (processes.empty()) {
732 oss << _("Nessun processo sta utilizzando webcam o microfono.") << "\n\n";
733 } else {
734 oss << _("📋 Processi attivi:\n\n");
735 for (const auto &p : processes)
736 oss << " • " << p << "\n";
737 oss << "\n";
738 }
739
740
741 oss << "────────────────────────────────────────────\n";
742 oss << _("📜 Log giornaliero:\n\n");
743
744
745 std::string base_dir = Glib::get_home_dir() + "/.local/share/BastionGuard/logs";
746 auto t = std::time(nullptr);
747 std::tm tm{};
748 localtime_r(&t, &tm); // thread-safe rispetto a std::localtime
749 char datebuf[16];
750 std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d", &tm);
751 std::string log_path = base_dir + "/privacyd_" + std::string(datebuf) + ".log";
752
753 if (fs::exists(log_path)) {
754 std::ifstream log_file(log_path);
755 if (log_file.is_open()) {
756 std::stringstream buffer;
757 buffer << log_file.rdbuf();
758 oss << buffer.str();
759 } else {
760 oss << _("⚠️ Impossibile leggere il log giornaliero.") << "\n";
761 }
762 } else {
763 oss << _("📝 Nessun log disponibile per oggi.") << "\n";
764 }
765
766
767 Glib::signal_idle().connect_once([this, alive, txt = oss.str()]() {
768 BastionGuard::if_alive(alive, [this, &txt]() {
769 txt_processes_.get_buffer()->set_text(txt);
770 lbl_status_.set_text(_("✅ Aggiornamento completato."));
771 });
772 });
773
774 } catch (const std::exception &e) {
775 std::string err = std::string(_("Errore update_process_list: ")) + e.what();
776 write_log(err);
777 }
778 }).detach();
779}
780
781
782
783void PrivacyPage::write_log(const std::string& msg) {
784 std::thread([msg]() {
785 try {
786 std::string base_dir = Glib::get_home_dir() + "/.local/share/BastionGuard/logs";
787 if (!fs::exists(base_dir))
788 fs::create_directories(base_dir);
789
790
791 auto t = std::time(nullptr);
792 std::tm tm{};
793 localtime_r(&t, &tm); // thread-safe rispetto a std::localtime
794 char datebuf[16];
795 std::strftime(datebuf, sizeof(datebuf), "%Y-%m-%d", &tm);
796 std::string log_path = base_dir + "/privacyd_" + std::string(datebuf) + ".log";
797
798
799 std::ofstream log(log_path, std::ios::app);
800 if (log.is_open()) {
801 char timebuf[32];
802 std::strftime(timebuf, sizeof(timebuf), "%H:%M:%S", &tm);
803 log << "[" << timebuf << "] " << msg << std::endl;
804 }
805
806
807 const auto now = std::chrono::system_clock::now();
808 const auto seven_days = std::chrono::hours(24 * 7);
809
810 for (const auto& entry : fs::directory_iterator(base_dir)) {
811 if (entry.is_regular_file()) {
812 auto ftime = fs::last_write_time(entry);
813 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
814 ftime - fs::file_time_type::clock::now()
815 + std::chrono::system_clock::now());
816 auto age = now - sctp;
817 if (age > seven_days) {
818 try {
819 fs::remove(entry.path());
820 } catch (...) {
821
822 }
823 }
824 }
825 }
826
827 } catch (...) {
828 std::cerr << _("[Privacy] Errore scrittura log giornaliero.") << std::endl;
829 }
830 }).detach();
831}
832
833
834
836 try {
837 auto keyfile = Glib::KeyFile::create();
838 auto config_dir = Glib::get_home_dir() + "/.config/BastionGuard";
839 if (!std::filesystem::exists(config_dir))
840 std::filesystem::create_directories(config_dir);
841
842 std::string config_path = config_dir + "/privacy.conf";
843 keyfile->set_boolean("privacy", "enabled", enabled);
844 keyfile->save_to_file(config_path);
845 write_log(enabled ? _("Privacy abilitata") : _("Privacy disabilitata"));
846 } catch (const Glib::Error &e) {
847 std::cerr << _("[Privacy] Errore salvataggio config: ") << e.what() << std::endl;
848 }
849}
850
852 try {
853 auto keyfile = Glib::KeyFile::create();
854 std::string config_path = Glib::get_home_dir() + "/.config/BastionGuard/privacy.conf";
855 if (Glib::file_test(config_path, Glib::FileTest::EXISTS)) {
856 keyfile->load_from_file(config_path);
857 return keyfile->get_boolean("privacy", "enabled");
858 }
859 } catch (...) {
860 std::cerr << _("[Privacy] Errore caricamento config") << std::endl;
861 }
862 return true;
863}
864
866 if (toggle_busy_.load())
867 return;
868
869 const bool active = switch_webcam_.get_active();
870 save_privacy_state(active);
871
872 switch_webcam_.set_sensitive(false);
873 lbl_status_.set_text(active
874 ? _("📷 Riattivazione webcam/microfono...")
875 : _("🔒 Disattivazione webcam/microfono in corso..."));
876
877 write_log(std::string(_("Toggle privacy: ")) + (active ? "ON (unblock)" : "OFF (block)"));
878
879 toggle_busy_ = true;
881}
882
883
884
886 auto alive = alive_;
887 std::thread([this, alive, unblock]() {
888
889 const bool ok = run_privhelper_pkexec(unblock ? "unblock" : "block");
890
891
892 Glib::signal_idle().connect_once([this, alive, ok, unblock]() {
893 BastionGuard::if_alive(alive, [this, ok, unblock]() {
894 if (ok) {
895 if (unblock) {
896
897 lbl_status_.set_text(_("🕵️ Monitoraggio privacy attivo"));
898 write_log(_("Script OK: UNBLOCK eseguito"));
899 monitor_running_ = true;
900 if (!monitor_thread_.joinable()) {
902 }
903 } else {
904
905 lbl_status_.set_text(_("🔕 Privacy monitor disattivato"));
906 write_log(_("Script OK: BLOCK eseguito"));
907 monitor_running_ = false;
908 if (monitor_thread_.joinable()) {
909 monitor_thread_.join();
910 }
911 }
912 } else {
913
914 write_log(_("ERRORE: esecuzione script privacy fallita"));
915 lbl_status_.set_text(_("❌ Errore nell'esecuzione dello script."));
916
917 switch_webcam_.set_active(!unblock);
918 }
919
920
922 switch_webcam_.set_sensitive(true);
923 toggle_busy_ = false;
924 });
925 });
926 }).detach();
927}
928
930 auto alive = alive_;
931 std::thread([this, alive]() {
932 const bool ok = run_privhelper_pkexec("unblock");
933
934 Glib::signal_idle().connect_once([this, alive, ok]() {
935 BastionGuard::if_alive(alive, [this, ok]() {
936 if (ok) {
937 write_log(_("🔓 Dispositivi sbloccati manualmente"));
938 devices_blocked_ = false;
939 btn_unlock_.set_visible(false);
940 lbl_status_.set_text(_("Webcam e microfono sbloccati"));
941 } else {
942 write_log(_("❌ Errore durante sblocco manuale"));
943 lbl_status_.set_text(_("❌ Impossibile sbloccare i dispositivi."));
944 }
945
947 });
948 });
949 }).detach();
950}
#define _(s)
static bool run_privhelper_pkexec(const std::string &action)
static std::string read_exe_path(const std::string &pid_str)
std::queue< std::string > alert_queue_
static std::string read_cmdline(const std::string &path)
static int read_ppid(const std::string &pid_str)
static bool parent_is_noise(const std::string &pid_str)
static bool is_noise_process(const std::string &comm, const std::string &exe, const std::string &cmdline)
static std::string read_text_file(const std::string &path)
Gtk::Window * current_alert_
std::atomic< bool > toggle_busy_
BastionGuard::AliveFlag alive_
bool load_privacy_state()
std::atomic< bool > devices_blocked_
void start_monitor_thread()
Gtk::TextView txt_processes_
void on_toggle_privacy()
void monitor_loop()
void show_permission_popup(const std::string &process_name)
void save_privacy_state(bool enabled)
Gtk::Switch switch_webcam_
Gtk::Button btn_unlock_
Gtk::Button btn_refresh_
sigc::connection auto_refresh_conn_
void run_privacy_script_async(bool unblock)
void show_next_alert()
Gtk::Label lbl_title_
Gtk::ScrolledWindow scroller_
void unblock_devices_local()
void update_process_list()
std::atomic< bool > monitor_running_
std::vector< std::string > detect_active_devices()
void write_log(const std::string &msg)
std::thread monitor_thread_
std::set< std::string > known_processes_
void on_refresh_clicked()
Gtk::Label lbl_status_
void start_privacy_daemon()
Gtk::CheckButton chk_auto_refresh_
void setup_auto_refresh_toggle()
void manual_unlock_devices()
bool is_daemon_active()
void if_alive(const AliveFlag &alive, Fn &&fn)
Result run(const std::vector< std::string > &args, bool merge_stderr=false, std::size_t limit=8U *1024U *1024U, const std::map< std::string, std::string > &environment={}, const std::function< bool(const std::string &)> &on_line={})
std::string resource(const std::string &rel)
Definition Resource.hpp:31